raf聚合dom修改的核心是批量提交写操作以对齐浏览器渲染节拍。通过队列缓存变更、单次raf触发批量执行,避免同步布局,读取操作需提前预读缓存,不可在flush中进行。

使用 requestAnimationFrame(简称 rAF)聚合 DOM 修改,核心是把多次零散的修改“攒起来”,等浏览器下一次重绘前统一执行,避免强制同步布局(layout)和重绘(paint)带来的性能损耗。关键不在“用不用 rAF”,而在于“如何用它协调时机 + 批量提交”。
理解浏览器渲染节拍与 DOM 修改的冲突
浏览器通常以约 60fps(每 16.7ms 一帧)运行渲染流水线:JS 执行 → 样式计算 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)。当你在 JS 中连续读写 DOM 属性(如先 el.offsetWidth 再改 el.style.left),浏览器可能被迫提前触发同步布局(reflow),打断当前帧节奏,造成卡顿。
解决思路不是禁止 DOM 操作,而是让所有读写操作对齐到帧起点,并集中处理写操作,把“读-写-读-写”的交错模式,变成“批量读 → 批量写 → 一次渲染”。
用 rAF 实现 DOM 修改聚合的基本模式
典型做法是维护一个待更新队列,在任意时刻调用更新函数时,不立即操作 DOM,而是将变更任务推入队列,并确保最多只调度一次 rAF。当 rAF 回调触发时,清空队列、批量执行所有写操作。
在 Linux 上通过 Chrome 挂载/VNC 自动发布小红书长笔记。Agent 撰写标题和正文后执行 xhs_publish.sh --publish --submit,触发小红书发布。
- 用一个
Set或数组缓存待更新的元素或变更描述(如{el, prop, value}) - 用布尔标志(如
isQueued)控制是否已请求 rAF,避免重复注册 - rAF 回调中清空队列、遍历执行所有 DOM 写操作(仅写,不读)
- 如需读取布局信息(如尺寸、位置),必须在 rAF 回调之前、且仅一次完成(例如在上一帧末尾或事件处理中预读并缓存)
一个轻量实用的聚合更新工具示例
以下是一个可直接使用的简易封装:
const batchDOM = {
queue: new Set(),
isQueued: false,
schedule() {
if (!this.isQueued) {
this.isQueued = true;
requestAnimationFrame(() => this.flush());
}
},
update(el, styles) {
this.queue.add({ el, styles });
this.schedule();
},
flush() {
this.queue.forEach(({ el, styles }) => {
Object.assign(el.style, styles);
});
this.queue.clear();
this.isQueued = false;
}
};
// 使用示例
batchDOM.update(div1, { opacity: 0.5, transform: 'translateX(10px)' });
batchDOM.update(div2, { left: '20px', top: '30px' });
batchDOM.update(div3, { display: 'none' });
// 三次调用只触发一次 rAF,一次样式批量写入
注意:该示例只处理写操作。若需根据某元素宽度动态设置另一元素高度,应先在事件中读取宽度并保存,再传入 update;不要在 flush 中读取,否则会触发强制同步布局。
进阶建议:结合 MutationObserver 或框架响应式原理
在复杂应用中,可将 rAF 聚合与状态变更解耦:
- 监听状态变化(如 Vue 的响应式 setter、React 的 setState 后的 effect),不直接操作 DOM,而是标记“需更新”
- 用 rAF 统一驱动视图刷新,内部按组件/区域分组批量 patch DOM
- 对高频触发场景(如滚动、鼠标移动),可用
raf-throttle进一步限制更新频率,避免 rAF 被频繁重置 - 现代方案如 React 的
useTransition或 Vue 的v-memo,底层也依赖类似节拍控制,可优先考虑框架原生优化能力










