你给一个元素悄悄改了宽度,结果整个页面都抖了一下?你加了个动画,电脑风扇开始狂转?今天我们来认识浏览器渲染里的"三兄弟"------重排、重绘、合成。弄懂它们,你就能写出流畅60帧的页面,告别卡顿。
前言
想象一下,你家客厅要重新装修。你只是换了个抱枕(重绘),很轻松。但如果你要把墙拆了(重排),那得搬家具、砸墙、重新粉刷,累得半死。如果只是把电视画面换个图层(合成),连工人都不要,遥控器一按就行。
浏览器的渲染也是这个道理。理解这三种操作的成本,就能写出性能飞起的页面。
一、先复习:渲染流水线
之前我们讲过,浏览器把HTML/CSS变成屏幕上的像素,要经过:DOM树 + CSSOM树 → 渲染树 → 布局(计算位置大小)→ 绘制(填充像素)→ 合成(合并图层)。
其中:
- 重排(Reflow):重新计算布局(位置、大小)。成本最高。
- 重绘(Repaint):重新绘制像素(颜色、背景、阴影等)。成本中等。
- 合成(Composite):重新合并图层。成本极低(走GPU)。
二、重排:动到筋骨,全员遭殃
什么操作会触发重排?
- 改变元素的几何属性 :
width、height、margin、padding、border、top、left...... - 改变DOM结构:增删元素、改变内容(文字变了导致高度变化)。
- 读取某些布局属性 :
offsetTop、scrollTop、clientWidth、getComputedStyle()。因为浏览器需要返回最新值,不得不强制重排。 - 改变窗口大小(resize事件)。
- 激活伪类 (如
:hover导致样式变化影响布局)。
重排的代价:浏览器要重新计算整个或部分渲染树,然后重新布局、绘制、合成。就像你拆了一面墙,整个房子都得重新量尺寸。
三、重绘:只换皮肤,不动骨架
什么操作会触发重绘但不触发重排?
- 改变颜色 :
color、background-color、border-color、box-shadow等。 - 改变可见性 :
visibility(但display: none会触发重排)。 - 改变背景图 、
outline等。
重绘的代价:不需要重新布局,但还是要重新绘制像素,比重排轻,但也不是免费。
四、合成:GPU加速的"超车道"
合成是成本最低的环节,因为它不涉及布局和绘制,只把已有的图层合并。能触发合成的属性有:
transform(平移、旋转、缩放)opacityfilter
当你用transform: translateZ(0)或will-change: transform时,浏览器会把这个元素提升到单独的合成层 ,后续动画只由GPU处理,完全不触发重排和重绘。这就是为什么动画推荐用transform而不是left。
css
/* 差:触发重排 */
.box {
transition: left 0.3s;
left: 0;
}
.box:hover {
left: 100px;
}
/* 好:只触发合成 */
.box {
transition: transform 0.3s;
transform: translateX(0);
}
.box:hover {
transform: translateX(100px);
}
五、如何减少重排和重绘?
1. 批量修改样式
不要挨个改属性,用class一次改完:
js
// 差
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
// 好
element.classList.add('new-size');
2. 让元素脱离文档流再操作
比如要插入多个li,可以先隐藏(display: none),改完再显示,只触发两次重排。
js
const ul = document.getElementById('list');
ul.style.display = 'none';
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = i;
ul.appendChild(li);
}
ul.style.display = 'block';
3. 使用文档片段(DocumentFragment)
js
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = i;
fragment.appendChild(li);
}
ul.appendChild(fragment); // 只触发一次重排
4. 读写分离
不要交替读取和修改布局属性,否则会触发多次重排。
js
// 差
for (let i = 0; i < boxes.length; i++) {
boxes[i].style.width = boxes[i].offsetWidth + 'px'; // 读后立即写
}
// 好:先读后写
const widths = [];
for (let i = 0; i < boxes.length; i++) {
widths.push(boxes[i].offsetWidth);
}
for (let i = 0; i < boxes.length; i++) {
boxes[i].style.width = widths[i] + 'px';
}
5. 使用transform和opacity做动画
永远不要用left、top、width、margin做动画,改用transform。
6. 固定元素位置
position: fixed或absolute的元素,其重排影响范围较小(只在自己层内)。
7. 避免使用table布局
一个小改动可能触发整个table的重排。
六、实战:一个性能优化的例子
假设你要做一个跟随鼠标移动的小光点(类似鼠标特效)。错误做法:每帧改变top/left,触发重排。正确做法:用transform。
js
// 差:每移动1px就重排一次
dot.style.left = x + 'px';
dot.style.top = y + 'px';
// 好:只触发合成
dot.style.transform = `translate(${x}px, ${y}px)`;
七、怎么分析页面重排/重绘?
Chrome DevTools → Performance 面板,录制一段操作,查看"Layout Shift"、"Paint"等标记。红色紫色区域越少越好。
八、总结:三兄弟的"饭量"
- 重排:吃满汉全席,最贵。动几何、DOM结构。
- 重绘:吃快餐,中等。动颜色、背景。
- 合成:喝矿泉水,几乎免费。动transform、opacity。
优化口诀:能用transform别用left,能用class别改style,读写分离,批量操作。
如果你觉得今天的"三兄弟"够形象,点个赞让更多人看到。明天我们将聊聊JavaScript引擎与内存管理,看看V8是怎么给代码"打扫卫生"的。我们明天见!