核心是分离读布局与写DOM操作,先批量读取所有尺寸位置信息,再批量执行样式和DOM修改;读操作应置于requestAnimationFrame回调开头以确保layout已完成,避免隐式触发强制重排。

核心是把“读布局”和“写 DOM”彻底隔开,不让它们在一次 JS 任务里来回穿插。浏览器能高效批量处理样式变更,但只要中间插一次读取 offsetHeight、getBoundingClientRect 这类属性,就会立刻强制刷新 layout 缓存,把前面的写、后面的写全拖进同步路径,帧率直接掉。
先批量读,再批量写
这是最基础也最关键的一步。所有需要的尺寸、位置信息,一次性读完;所有 class 切换、style 设置、DOM 插入,统一放在后面做。
- ❌ 错误模式:循环里每轮都 el.offsetHeight → el.style.width = ... → el.offsetTop → el.classList.add(...),N 个元素就触发 N 次强制 layout
- ✅ 正确做法:第一遍 forEach 收集所有 offsetHeight、offsetTop、getBoundingClientRect;第二遍 forEach 批量设置 style、class 或 appendChild
- 这样最多只触发 1 次 layout(甚至 0 次,如果浏览器发现没真变化)
把“读”推迟到 layout 完成之后
即使做了批量读写,如果“读”发生在上一帧未 flush 的样式修改之后,仍可能触发强制同步 layout。稳妥的做法是让读操作落在 requestAnimationFrame 回调开头。
- raf 回调执行时,浏览器已完成 layout,paint 尚未开始,此时读取尺寸是安全的
- 不要在 scroll 或 input 事件里直接读 offsetHeight;改用 raf 包裹读操作,或用 ResizeObserver / IntersectionObserver 替代轮询
- 例如:requestAnimationFrame(() => { const h = el.offsetHeight; /* 安全 */ });
避开隐蔽的 layout 触发点
有些操作表面不显眼,实则悄悄触发 layout,容易被忽略。
- getComputedStyle(el).width、.height、.left 等依赖几何计算的属性会 layout;但 .color、.fontSize 不会
- el.offsetParent === null 比 el.offsetParent == null 更可靠,后者在某些 CSS 下会误判挂载状态
- 元素还没插入文档时调用 getComputedStyle(el).height,返回 auto,但仍会触发 layout —— 即使你根本不需要这个值
- contain: layout 或 content-visibility: auto 不能当作“自动防抖”用;读它的尺寸照样强制计算
框架中也要手动拆开读写
像 React 的 useLayoutEffect 看似“同步”,但它仍是 JS 同步执行阶段,里面读尺寸仍是强制 layout。不能依赖它自动优化。
- 写操作放 useLayoutEffect(确保 DOM 已更新)
- 读操作改用 useEffect + requestAnimationFrame,或自定义 hook 封装“下帧读”逻辑
- Vue 的 onUpdated 里同样要避免立即读 clientWidth,需加 raf 延迟


















