为什么你的页面会卡顿
在 Web 开发中,页面流畅度直接影响用户体验。一个帧率稳定在 60fps 的页面,意味着每一帧的渲染时间必须控制在 16.6ms 以内。然而,很多开发者在编写 CSS 时,往往只关注视觉效果,忽略了样式属性对浏览器渲染管线的深层影响。理解浏览器渲染流程中的重排(Reflow)、重绘(Repaint)与合成层(Compositing Layer),是优化渲染性能的核心。
浏览器渲染管线概览
在讨论优化之前,先简单回顾浏览器从接收到 HTML/CSS/JS 到最终呈现像素的完整流程:
- 解析 HTML,构建 DOM 树。
- 解析 CSS,构建 CSSOM 树。
- 合并 DOM 与 CSSOM,生成渲染树(Render Tree)。
- 布局(Layout):计算每个节点在屏幕上的精确位置和尺寸。
- 绘制(Paint):将节点的视觉信息(颜色、阴影、边框等)填充为像素。
- 合成(Composite):将多个图层按正确顺序合并,最终输出到屏幕。
其中,第 4 步对应重排,第 5 步对应重绘,第 6 步则是合成层的用武之地。关键结论是:越靠后的阶段被触发,性能开销越小。
重排:代价最高的操作
重排,也叫回流,是指浏览器重新计算元素的几何属性——位置、尺寸、边距、对齐方式等。一旦某个元素的几何信息发生变化,浏览器可能需要重新计算整个渲染树中受影响的部分,甚至全部。
哪些操作会触发重排?
- 改变元素的宽高、内边距、外边距、边框宽度
- 修改字体大小或字体族
- 调整窗口大小(
resize事件) - 增删可见的 DOM 节点
- 读取某些布局属性,如
offsetWidth、offsetHeight、getComputedStyle()、scrollTop等
特别需要注意的是,强制同步布局(Forced Synchronous Layout)是性能杀手。当你在一次 JavaScript 执行中先修改样式,紧接着又读取布局属性,浏览器被迫立即执行重排以返回准确值,打断了原本可以批量处理的优化机制。
// 糟糕的写法:读写交替,触发多次强制同步布局
for (let i = 0; i < items.length; i++) {
items[i].style.width = container.offsetWidth + 'px';
}
// 优化写法:先读后写,只触发一次布局
const width = container.offsetWidth;
for (let i = 0; i < items.length; i++) {
items[i].style.width = width + 'px';
}
重绘:不涉及布局的视觉变化
重绘是指元素的几何属性没有改变,但外观发生了变化,例如颜色、背景色、透明度、阴影等。浏览器不需要重新计算布局,只需重新绘制受影响区域的像素。
重绘的开销通常比重排小,但并非可以忽略。如果重绘区域很大,或者触发频率很高(如动画中不断改变背景色),依然会造成明显的性能问题。
常见触发重绘的属性
color、background-color、background-imageborder-color、border-stylebox-shadow、text-shadowvisibility(注意:visibility: hidden触发重绘,display: none触发重排)
合成层:绕开重排与重绘的捷径
合成是渲染管线的最后一步。浏览器可以将某些元素提升为独立的合成层,这些图层由 GPU 单独处理,修改时无需触发主线程的重排或重绘。
如何创建合成层?
现代浏览器中,以下方式会创建合成层:
- 使用
transform: translateZ(0)或translate3d(0, 0, 0) - 使用
will-change: transform或will-change: opacity - 使用
opacity配合动画 - 使用
position: fixed - 使用
<video>、<canvas>等元素
其中,will-change 是最推荐的方式,因为它明确告诉浏览器“这个元素即将发生变化”,让浏览器提前做好图层提升的准备。
.animated-element {
will-change: transform;
transition: transform 0.3s ease;
}
.animated-element:hover {
transform: scale(1.1);
}
由于 transform 和 opacity 的变化只影响合成层,不会触发重排或重绘,因此它们是实现高性能动画的首选属性。
合成层的代价
合成层并非越多越好。每个合成层都需要占用 GPU 内存,过多的图层会导致内存暴涨,在移动设备上尤其明显。此外,图层提升还可能导致文本渲染模糊(在部分浏览器中)。因此,按需提升,用完即撤是基本原则。例如,动画结束后移除 will-change:
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});
实战优化策略
1. 批量修改样式
避免逐条修改样式属性,使用 cssText 或 class 切换来批量更新:
// 不推荐
el.style.width = '100px';
el.style.height = '200px';
el.style.margin = '10px';
// 推荐
el.classList.add('new-layout');
2. 使用 DocumentFragment 或离线 DOM
在内存中构建好 DOM 结构后再一次性插入,避免多次重排:
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
list.appendChild(fragment);
3. 动画优先使用 transform 和 opacity
这两个属性只触发合成,不触发重排和重绘。避免使用 top、left、width、height 等属性做动画。
4. 避免频繁读取布局属性
将读取操作集中在一起,写入操作集中在另一起,中间不要交替。
5. 使用 contain 属性隔离渲染
CSS 的 contain 属性可以告诉浏览器某个元素及其子树是独立的,限制重排和重绘的影响范围:
.widget {
contain: layout paint;
}
总结
渲染性能优化的本质,是尽可能让样式变化停留在渲染管线的后段。重排代价最高,重绘次之,合成最低。在实际开发中,应遵循以下优先级:
- 能用
transform/opacity动画,就不用布局属性。 - 能批量操作,就不逐条修改。
- 能隔离影响范围,就不让变化扩散。
- 能按需提升合成层,就不滥用
will-change。
掌握这些原则,结合 Chrome DevTools 的 Performance 面板进行实际测量,你就能构建出真正流畅的 Web 页面。
未经允许不得转载:任鹏个人博客 » CSS 渲染性能优化:重排、重绘与合成层


朋友圈点赞图在线生成源码