3、渲染流水线:从 JavaScript 到像素的完整路径
说实话,我刚入行那会儿,觉得浏览器渲染就是个黑盒。代码写完了,页面出来了,中间发生了什么?不知道。直到有一次,我接手了一个 H5 活动页——就一个简单的抽奖转盘,结果在低端机上卡得转不动。我打开 DevTools 的 Performance 面板一看,好家伙,每次点击都要等 800ms 才能看到转盘动一下。
那时候我才意识到:不理解渲染流水线,你连问题出在哪都不知道。
这一章,我就带你走一遍浏览器从 JS 到像素的完整路径。说白了,就是五个阶段:JavaScript → Style → Layout → Paint → Composite。每一步都可能成为性能瓶颈,每一步也都有优化空间。
核心观点:渲染流水线不是一条单向的管道,而是一条可以「短路」的路径。你改一个 CSS 属性,可能只触发最后两步;你改一个布局属性,就得从头跑到尾。理解这个差异,是优化的起点。
3.1 JavaScript:一切的起点
流水线的第一站,是 JavaScript。你写的任何交互逻辑、DOM 操作、动画控制,都从这里开始。
但这里有个常见的坑:你以为只改了数据,实际上触发了整个流水线。比如你用 element.style.width = '200px',浏览器不会立刻去算布局——它会先把你所有的 JS 执行完,然后统一处理。
我的习惯:在写动画或高频交互时,我会刻意把「数据计算」和「DOM 操作」分开。数据算完了,再一次性更新 DOM。这样浏览器只需要重排一次,而不是每次赋值都重排一次。
举个例子,你写了个循环去改 100 个元素的宽度:
// ❌ 糟糕的做法:每次循环都触发重排
for (let i = 0; i < 100; i++) {
items[i].style.width = i * 2 + 'px';
}
// ✅ 更好的做法:用 requestAnimationFrame 批量更新
requestAnimationFrame(() => {
for (let i = 0; i < 100; i++) {
items[i].style.width = i * 2 + 'px';
}
});
为什么 requestAnimationFrame 更好?因为它告诉浏览器:「我要在下一帧开始前更新这些样式,你帮我合并一下。」浏览器就会把这一批操作打包处理,而不是每改一个就重新算一遍。
3.2 Style:样式计算
JS 执行完了,浏览器开始算样式。这一步叫 Style 计算,也叫「样式递归」。
说白了,就是浏览器拿着你的 CSS 选择器,去匹配每个 DOM 节点,算出它最终的样式。注意,是「最终」——所有继承、层叠、权重计算,都在这一步完成。
我在项目中遇到过一个问题:一个页面有 5000 多个 DOM 节点,每个节点都用了深层的后代选择器(比如 .wrapper .content .list .item a)。结果每次样式计算都要花 30ms 以上。30ms 看起来不多,但一帧总共才 16.6ms——直接超了。
避坑指南:选择器越复杂,匹配时间越长。尤其是「通配符选择器」和「深层后代选择器」,在大型 DOM 树里是性能杀手。我建议:能用 class 就用 class,别玩花哨的选择器嵌套。
| 选择器类型 | 示例 | 匹配效率 | 说明 |
|---|---|---|---|
| ID 选择器 | #header |
最快 | 直接通过哈希表查找 |
| 类选择器 | .item |
快 | 通过类名索引查找 |
| 标签选择器 | div |
中等 | 需要遍历标签集合 |
| 后代选择器 | .list a |
慢 | 需要向上遍历 DOM 树 |
| 通配符选择器 | * |
最慢 | 匹配所有元素,慎用 |
3.3 Layout:布局计算
样式算完了,浏览器知道了每个元素「长什么样」。但还不知道「放哪里」——这就是 Layout 要做的事。
Layout 阶段,浏览器会计算每个元素的几何信息:宽、高、x、y 坐标。这个过程也叫 重排(Reflow)。
你想想看,一个元素宽度变了,它后面的元素可能都得跟着挪位置。这就是为什么修改布局属性代价那么大——牵一发而动全身。
关键认知:Layout 是流水线中最耗时的阶段。一个复杂的页面,Layout 可能占掉整帧时间的 40%-60%。而且它是「阻塞式」的——Layout 没算完,后面的 Paint 和 Composite 都得等着。
我记得有一次优化一个表格页面,每次排序都要卡 200ms。打开 Performance 一看,Layout 占了 150ms。原因是什么?排序后我用了 innerHTML 重新渲染整个表格,浏览器不得不重新计算所有单元格的位置。
后来改成只更新数据变化的那几行,Layout 时间降到了 20ms。嗯,差别就这么大。
3.4 Paint:绘制
Layout 算完了位置,浏览器知道每个元素「放哪里」了。接下来要画出来——这就是 Paint 阶段。
Paint 做的事情很简单:把每个元素的视觉表现(颜色、背景、边框、阴影、文字)画到一张或多张「位图」上。你可以理解为,浏览器在内存里画了一幅画。
但这里有个细节:浏览器会把页面分成多个图层。每个图层独立绘制,最后再合成到一起。为什么要分层?因为如果整个页面只有一个图层,那任何一个小改动都得重绘整个页面——太浪费了。
我的经验:如果你发现某个动画导致整个页面闪烁,大概率是 Paint 区域太大了。可以用 will-change: transform 或 transform: translateZ(0) 把动画元素提升到独立图层。这样浏览器只需要重绘那个图层,其他部分不动。
Paint 的耗时取决于两个因素:
- 绘制区域的大小——区域越大,画得越慢
- 绘制内容的复杂度——渐变、阴影、圆角这些比纯色背景慢得多
举个例子,一个带 box-shadow 的弹窗,它的 Paint 时间可能是纯色弹窗的 3-5 倍。因为阴影需要计算模糊、扩散、偏移——这些都是 CPU 密集的操作。
3.5 Composite:合成
最后一步,合成。
浏览器把各个图层按照正确的顺序叠在一起,输出到屏幕上。这一步叫 Composite。
合成的关键优势是:它不需要重绘。如果你只改了 transform 或 opacity,浏览器只需要在合成阶段重新排列一下图层的位置或透明度——之前的绘制结果可以直接复用。
这就是为什么 transform 动画那么流畅。它跳过了 Layout 和 Paint,只走 Composite。你想想看,从五步变成一步,能不流畅吗?
注意:不是所有属性都能触发 Composite-only 路径。只有 transform 和 opacity 是「安全的」。其他属性,比如 left、top、width、height,都会触发 Layout 或 Paint。
3.6 如何查看流水线耗时
光说不练假把式。我建议你打开 Chrome DevTools 的 Performance 面板,录一段操作看看。
具体步骤:
- 按 F12 打开 DevTools,切换到 Performance 面板
- 点击左上角的录制按钮(圆点)
- 在页面上执行你想要分析的操作(比如点击、滚动、动画)
- 点击停止录制
你会看到一条时间轴,上面标着每个阶段的耗时:
- 蓝色:网络和 HTML 解析
- 紫色:样式计算和布局
- 绿色:绘制
- 黄色:合成
我曾经用这个工具发现了一个隐藏很深的性能问题:一个看似简单的滚动动画,每次滚动都触发了全页面的 Layout。原因是一个固定定位的元素用了 top: calc(50% - 100px),每次滚动都要重新计算。改成 transform: translateY(-50%) 后,Layout 直接消失了。
总结一下:
- 修改布局属性 → 触发完整流水线(最慢)
- 修改绘制属性 → 跳过 Layout(中等)
- 修改合成属性 → 只走 Composite(最快)
优化的核心思路就是:尽量走短路径。能用 transform 就别用 left,能用 opacity 就别用 visibility。
嗯,这一章的内容就到这儿。理解了这个流水线,你再看 Performance 面板里的那些色块,就不再是「天书」了。每一块颜色背后,都是浏览器在替你干活——而你要做的,就是让它干得少一点、快一点。