Kodokon kodokon.com

渲染管线:回流、重绘与合成

理解浏览器渲染的四个阶段,从而准确知道哪些 CSS 属性开销高,以及 will-change 在底层是如何工作的。

11 分钟 · 3 题

在 Kodokon 中打开本课

浏览器通过一条四阶段的管线把你的 CSS 变成像素:Style(计算取值)、Layout(几何计算,也就是回流)、Paint(栅格化成像素,即重绘)和 Composite(在 GPU 上组装图层)。黄金法则是:你触及的阶段越靠前,代价就越大,因为后续的每一个阶段都要重新执行。修改 widthtop 会触发布局 + 绘制 + 合成。修改 colorbox-shadow 会跳过布局但需要重绘。只有 transformopacity 能够仅由合成器线程处理:即使主线程被 JavaScript 阻塞,动画依然保持流畅。

CSS
.slide-bad {
  position: absolute;
  transition: left 0.3s ease;
}
.slide-bad:hover { left: 120px; }

.slide-good {
  transition: transform 0.3s ease;
}
.slide-good:hover { transform: translateX(120px); }
两个视觉上完全相同的动画:第一个在每一帧都强制回流,第二个则停留在合成器上。

浏览器很懒:它会把你的样式写入批量处理,只在下一次刷新时才重新计算布局。但某些 JavaScript 读取操作——offsetHeightgetBoundingClientRect()scrollTop、对几何属性调用 getComputedStyle()——要求获取最新的数值。如果此时有待处理的写入,浏览器就必须立即且同步地重新计算:这就是强制回流。在循环中交替读写会造成布局抖动:每次迭代都会触发一次完整回流,而不是整批只回流一次。

JAVASCRIPT
const items = document.querySelectorAll('.item');

items.forEach((item) => {
  const h = item.offsetHeight;
  item.style.height = h * 2 + 'px';
});

const heights = [...items].map((i) => i.offsetHeight);
items.forEach((item, index) => {
  item.style.height = heights[index] * 2 + 'px';
});
版本 1:每个元素触发一次强制回流。版本 2:先全部读取,再全部写入——只回流一次。

合成会把页面切分成若干图层,各自独立栅格化,再由 GPU 组装。will-change: transform 要求在动画开始之前就把元素提升到独立的图层,从而避免在第一帧付出昂贵的栅格化代价。一个常被忽视的规范细节:will-change 必须产生与它所声明的属性相同的副作用。因此 will-change: transform 会创建一个层叠上下文,并把元素变成其 position: fixed 后代的包含块——就和真正的 transform 一模一样。一个简单的性能提示因此可能破坏你的层叠顺序或固定定位元素。

JAVASCRIPT
const panel = document.querySelector('.panel');

panel.addEventListener('pointerenter', () => {
  panel.style.willChange = 'transform';
});

panel.addEventListener('transitionend', () => {
  panel.style.willChange = 'auto';
});
规范推荐的模式:动画开始前才提升,动画结束后立即释放。

知识检测

确认你已牢记本课的重点内容。

  1. 哪个动画能完全由合成器线程处理,既不回流也不重绘?
    • height 从 0 变到 200px
    • margin-left 从 0 变到 100px
    • transform: translateX(100px)
    • 悬停时的 box-shadow
  2. 为什么在一个同时修改样式的循环里读取 offsetHeight 会如此昂贵?
    • 每次读取都会在有待处理写入时强制一次同步回流
    • offsetHeight 每次访问都会触发一次 GPU 重绘
    • 该属性从不会被 JavaScript 引擎缓存
  3. will-change: transform 与真正的 transform 共享哪一个副作用?
    • 它会禁用元素的 CSS 过渡
    • 它会为 position: fixed 创建一个层叠上下文和一个包含块
    • 它会强制元素变成 display: block
    • 它会让栅格化分辨率翻倍