结论:减少重排(reflow,也叫 layout,浏览器重新计算元素几何位置)和重绘(repaint,重新画像素)的核心只有三条——**别在读布局属性之间穿插写样式、把动画交给 transform/opacity 走合成层、把重排的影响范围用 CSS 隔离在最小容器里**。做到这三点,绝大多数页面的 Layout 耗时能压到 60fps 预算(每帧 16.7ms)以内。
重排和重绘有什么区别?
结论:重排一定伴随重绘,重绘不一定触发重排,所以优化的优先级永远是「先消灭重排,再减少重绘」。
浏览器一帧的流程是 JS → Style → Layout → Paint → Composite。元素改了 width、height、padding、margin、font-size、display、position 这类影响几何的属性,浏览器要把 Layout 阶段重跑一遍,这就是重排;只改 color、background-color、visibility、box-shadow,几何不变,只重跑 Paint,这就是重绘。
关键区别在于代价:一次重排可能连带整棵子树甚至整个文档重新计算,尤其在 table 布局里,改一个单元格可能让整张表重排;重绘只影响那块像素区域。Chrome DevTools 的 Performance 面板里,Layout 是紫色块、Paint 是绿色块,Rendering 面板勾上「Paint flashing」会把你页面上所有正在重绘的区域用绿框标出来,勾上「Layout Shift Regions」则用蓝框标重排区域——这是最直接的诊断方式,先看再改。
哪些 JS 操作会触发「强制同步布局」?
结论:在一帧内「先写样式、再读布局属性」,会让浏览器被迫立刻执行一次同步 Layout,这就是布局抖动(layout thrashing),是最常见的性能杀手。
会强制布局的读写 API 包括:offsetTop/offsetLeft/offsetWidth/offsetHeight、clientTop/clientWidth/clientHeight、scrollTop/scrollLeft/scrollWidth/scrollHeight、getBoundingClientRect()、getComputedStyle()、offsetParent、innerText,以及 scrollIntoView()、focus() 的部分调用。
典型坏代码是在循环里交错读写:
// 反例:每次循环强制一次同步布局,100 个元素 = 100 次 reflow
for (const el of items) {
el.style.width = el.offsetWidth + 10 + 'px';
}
正确做法是读写分离,把所有读操作先跑完,再统一写:
// 正例:读一批 → 写一批,合并成一次布局
const widths = items.map(el => el.offsetWidth);
items.forEach((el, i) => { el.style.width = widths[i] + 10 + 'px'; });
如果读写逻辑天然交织(比如拖拽、滚动联动),用 requestAnimationFrame 把写操作推到下一帧,或者用 FastDOM 这类库自动分桶。另外:同一任务内的多次 style 修改会被浏览器自动合并成一次 Layout,只要中间不插入布局读取,就不必为「改了 10 次样式」焦虑。
怎么用 CSS 把重排的影响范围限制住?
结论:让频繁变化的元素脱离文档流、缩小它的布局容器,能把一次重排从「整页」降到「一个小方块」。
具体做法有四条:
- 脱离文档流:把动画元素设为
position: absolute 或 fixed,它变化时不会推挤兄弟节点。注意 transform 会创建新的包含块,如果里面有 position: fixed 子元素,定位基准会变。
- 用 CSS Containment 隔离:给独立组件加
contain: layout paint;,浏览器就知道这棵子树的布局变化不会外溢,可以把重排锁在容器内部。对长列表更狠的是 content-visibility: auto 配合 contain-intrinsic-size: 0 500px;,屏幕外的内容直接跳过布局和渲染。
- 避开 table 布局:
<table> 的自动布局算法在内容变化时可能重排整张表,列表类 UI 改用 display: grid 或 flex。
- 给图片和 iframe 写死尺寸:
<img width="800" height="600"> 或 CSS aspect-ratio,避免图片加载完成后把下面的内容整体推下去——这既是重排问题也是 CLS(累积布局偏移,Google 的核心网页指标,良好阈值是小于 0.1)问题。
动画为什么优先用 transform 和 opacity?
结论:transform 和 opacity 是仅有的两个能只在合成线程(compositor)处理的属性,改它们既不触发 Layout 也不触发 Paint。
前提是元素被提升为合成层。可以提前用 will-change: transform; 或 transform: translateZ(0); 声明,但别滥用:每个合成层都占显存,页面上出现几十上百个层会导致内存暴涨反而更卡,动画结束后应当移除 will-change。
反过来,动画里用 left、top、width、margin-left 做位移,每一帧都要走一次 Layout + Paint,几十个元素时会稳掉帧。同理,滚动监听里不要直接读 scrollTop 后立即写样式,用 { passive: true } 加 rAF 节流。
一套可以照抄的优化清单
结论:按下面 6 步走,能覆盖 90% 的重排重绘问题。
- 打开 DevTools Performance 录制 5 秒交互,找紫色 Layout 长条,点进去看调用栈里有没有
Forced reflow。
- 把所有「读布局 → 写样式」的循环改成「先读后写」,或用 rAF 拆帧。
- 批量 DOM 操作用
DocumentFragment 拼好后一次插入,避免 appendChild 调 100 次。
- 频繁动画元素加
position: absolute + will-change: transform,动画只改 transform/opacity。
- 独立组件加
contain: layout paint,长列表加 content-visibility: auto。
- 图片、视频、iframe 全部写死宽高或
aspect-ratio;自定义字体加 font-display: swap 减少字体加载引发的重排。
另外提醒一句:不要花时间优化 CSS 选择器复杂度。现代浏览器的选择器匹配早就不是瓶颈,把精力放在布局抖动和动画属性上,收益高一个数量级。
重排重绘优化的本质是「让浏览器少算、少画、少被逼着同步算」。先诊断(Performance + Paint flashing),再分离读写、限定范围、转移动画到合成层,顺序不能反。