渲染管线:布局 / 绘制 / 合成

本节目标

渲染管线(Render Pipeline):当 DOM 和 CSSOM 都就绪,浏览器要把它们变成屏幕上的像素,要走过这几步:

DOM + CSSOM
  → ① 样式计算(Style)   :匹配 CSS 选择器,算出每个节点的最终样式
  → ② 布局(Layout)      :算出每个元素的几何位置与尺寸(盒模型)
  → ③ 绘制(Paint)       :把可见内容画成「绘制指令」(分层)
  → ④ 合成(Composite)   :合成线程把各图层拼成最终画面交给 GPU

图层(Layer):浏览器会把页面拆成多个图层(触发条件如:3D transform、will-change、video、canvas 等)。合成线程负责把图层拼合并交给 GPU——这就是「硬件加速」的本质。

为什么 transform/opacity 动画流畅:它们只影响「合成」这一步,不需要重算布局和重绘,由合成线程 + GPU 独立完成,主线程空闲着,所以不卡。

下面这个 HTML,打开后能直接看到两种动画的差异(右侧 transform 通常明显更顺滑):

<!-- 运行环境:任意现代浏览器 -->
<!-- 步骤:存为 brw-l5.html,用浏览器打开,观察两个方块移动是否一样顺 -->
<!-- 左侧用 left(触发 布局→绘制→合成 整条管线),右侧用 transform(只走合成) -->

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <style>
    .box { width: 60px; height: 60px; border-radius: 8px; position: absolute; }
    #a { background: #e34c26; top: 40px;  animation: moveLeft 2s linear infinite alternate; }
    #b { background: #3178c6; top: 140px; animation: moveTransform 2s linear infinite alternate; }
    /* 左侧:改 left —— 每次都要重新布局+绘制 */
    @keyframes moveLeft { from { left: 0; } to { left: 400px; } }
    /* 右侧:改 transform —— 只走合成线程 */
    @keyframes moveTransform { from { transform: translateX(0); } to { transform: translateX(400px); } }
    p { font-family: sans-serif; }
  </style>
</head>
<body>
  <p>左:改 left(整条管线) 右:改 transform(仅合成)</p>
  <div id="a" class="box"></div>
  <div id="b" class="box"></div>
</body>
</html>

哪些属性触发什么阶段(优化时照这张表选属性):

触发布局(Layout,最贵):width / height / margin / padding / top / left / font-size
触发绘制(Paint,中等):color / background / border-radius / box-shadow
只触发合成(Composite,最便宜):transform / opacity / filter(部分)

will-change 提示(提前告诉浏览器「这个元素要动」,让它先分层):

.box { will-change: transform; }   /* 但不要滥用,图层多了内存也涨 */

名词解释

布局(Layout / Reflow):浏览器算出「每个元素该放在哪、多大」的过程。它消费 DOM 结构 + CSS 样式,产出每个节点的几何盒。你可以把它理解成「装修时先确定每件家具摆哪」。offsetWidth、getBoundingClientRect() 这类读取几何信息的「方法」,会强制浏览器立刻做一次布局(见下一节)。

绘制(Paint):把元素的视觉内容(颜色、边框、阴影、文字)转成「绘制指令」,类似「按布局图给家具上色」。绘制本身不决定位置,只决定「长什么样」。

合成(Composite):把已经画好的各个「图层」像叠透明胶片一样拼成最终画面,由合成线程交给 GPU。它的「方法」是 layerize()(分层)+ composite()(拼合)。关键点:合成不依赖主线程,所以 transform/opacity 动画能绕开主线程卡顿。

课后练习

练习 1:给一个元素加 transition: all 0.3s,然后 JS 里不断改它的 width。为什么动画会卡?该改成什么属性能丝滑?

答案:width 改变会触发布局(Layout)→ 绘制(Paint)→ 合成(Composite)整条管线,每一帧都重排重绘,主线程压力大,所以卡。应改用 transform: scale() 或 transform: translateX() 来做视觉变化——它们只走合成,由 GPU 完成,主线程空闲,动画丝滑。

练习 2:will-change: transform 是「写得越多越好」吗?会有什么副作用?

答案:不是。浏览器会为声明了 will-change 的元素提前创建独立图层,图层要占 GPU 内存。大量滥用会制造成百上千个图层,内存暴涨,反而拖慢合成甚至引发崩溃。正确做法:只在「确实要动、且动画期间」使用,动画结束可考虑移除(如动画 animationend 后清掉 will-change)。

总结

这一节的核心思想,我愿意把它概括成一句话:动画顺不顺畅,取决于你「惊动了」管线的哪一段。改 left 是把布局、绘制、合成全惊动一遍;改 transform 只是轻轻碰一下最末尾的合成。浏览器把「合成」单独丢给合成线程 + GPU,就是专门给动画留的一条「快车道」。但快车道不是免费的——will-change 提前分层要占显存,滥用会反噬。所以真正的工程智慧是:默认走合成属性做动效,必要时才分层,且用完即收。记住:你不是在「让动画变快」,而是在「别去惊动那条本就拥堵的主线程」。