2. 渲染机制基础:浏览器渲染流水线
做界面移植和页面切换优化,绕不开一个核心问题:浏览器到底是怎么把代码变成像素的?
我见过不少同学,一上来就怼 CSS 动画、怼 transform,效果没出来,反而更卡了。说白了,你不懂渲染流水线,就不知道瓶颈在哪。
这一节,我们就把浏览器渲染的底牌翻出来看看。
2.1 从 URL 到像素:渲染流水线全景
浏览器拿到 HTML 和 CSS 后,不是一口气画出来的。它走了一条固定的流水线。我个人习惯把它拆成五个阶段:
- 构建 DOM 树 — 解析 HTML,生成节点树
- 构建 CSSOM 树 — 解析 CSS,生成样式规则树
- 构建渲染树 — 合并 DOM + CSSOM,只保留可见节点
- 布局(Layout) — 计算每个节点的几何位置和尺寸
- 绘制(Paint) — 把像素画到屏幕上
嗯,这里要注意:不是每次更新都会走完这五步。有些操作只触发后面几步,这就是优化的空间。
核心记忆点:渲染流水线是“增量”的。你改一个颜色,浏览器可能只走 Layout + Paint;你改 transform,它可能连 Layout 都跳过。
2.2 深入理解每个阶段
2.2.1 DOM 树 — 结构骨架
浏览器解析 HTML 标签,生成一棵树。每个标签是一个节点,嵌套关系就是父子关系。
举个例子:
<div id="app">
<h1>标题</h1>
<p>正文内容</p>
</div>
解析后,DOM 树大概是:document → html → body → div#app → [h1, p]。
避坑:我曾经遇到一个项目,页面有 8000+ 个 DOM 节点,光构建 DOM 树就花了 120ms。后来用虚拟滚动砍到 200 个节点,首屏速度直接翻倍。DOM 节点数量,是性能的第一道坎。
2.2.2 CSSOM 树 — 样式规则
CSS 解析后生成 CSSOM 树。它和 DOM 树是并行构建的,但 CSSOM 会阻塞渲染。
为什么?因为浏览器必须等所有 CSS 下载并解析完,才知道每个元素长什么样。你想想看,如果 CSS 没加载完就渲染,页面会闪一下无样式内容(FOUC)。
我记得 Chrome 团队做过测试:CSSOM 构建时间与 CSS 文件大小成正比。一个 50KB 的 CSS 文件,解析时间大约在 10-15ms。看起来不多,但在移动端低端机上,这个数字会翻倍。
2.2.3 渲染树 — 可见节点的合并
DOM 树 + CSSOM 树 → 渲染树。这一步做了两件事:
- 合并样式:计算每个节点的最终样式(computed style)
- 剔除不可见节点:
display: none的节点、<head>里的节点,都不进渲染树
注意:visibility: hidden 和 opacity: 0 的节点会进入渲染树,因为它们仍然占据空间。这个区别很重要,后面讲 GPU 加速时会用到。
2.2.4 布局(Layout)— 算位置
布局阶段,浏览器遍历渲染树,计算每个节点的几何信息:宽、高、x、y。
这个阶段很重。为什么?因为一个元素的尺寸变化,可能引发级联重排。比如你改了某个元素的宽度,它的父元素、兄弟元素、子元素都可能要重新计算。
性能杀手:强制同步布局(Forced Synchronous Layout)。当你在 JS 中读取一个样式属性(比如 element.offsetHeight),而浏览器还没来得及计算布局时,它会立即强制执行一次布局。这在循环中尤其致命。
我曾经接手过一个项目,一个滚动列表里每帧都读 scrollTop 和 offsetHeight,导致每帧都触发强制布局。优化后改成只读一次缓存起来,帧率从 15fps 直接拉到 55fps。
2.2.5 绘制(Paint)— 画像素
布局完成后,浏览器把每个节点绘制成像素。绘制不是一次性画完的,它会把页面分成多个图层,每个图层独立绘制,最后合成。
绘制阶段的工作量取决于:
- 绘制区域的大小
- 绘制内容的复杂度(渐变、阴影、圆角等)
- 图层的数量
2.3 GPU 加速原理
好,到这里你可能会问:GPU 加速到底加速了什么?
说白了,GPU 加速就是把某些渲染操作从 CPU 搬到 GPU 上执行。GPU 擅长并行计算,特别适合处理像素和纹理。
在浏览器里,GPU 加速主要体现在合成阶段。当一个元素被提升为独立图层(比如用了 will-change: transform 或 transform: translateZ(0)),它的绘制结果会被缓存为一张纹理。后续对这个元素的变换(移动、旋转、缩放),只需要 GPU 对纹理做矩阵运算,不需要重新布局和绘制。
我举个例子:
/* 触发 GPU 加速的常见写法 */
.card {
transform: translateZ(0); /* 强制提升为图层 */
/* 或 */
will-change: transform; /* 提示浏览器提前准备 */
}
用了这个之后,对 .card 做 transform: translateX(100px),浏览器只走合成阶段,跳过 Layout 和 Paint。帧率从 30fps 到 60fps,就是这么来的。
但要注意:图层不是越多越好。每个图层都会占用 GPU 内存。移动端 GPU 内存通常只有 256MB-512MB,图层太多会导致内存溢出,反而卡顿。我建议图层数量控制在 10 个以内。
2.4 帧率与刷新率
这两个概念经常被混用,我帮你理清楚。
| 概念 | 定义 | 典型值 | 谁决定的 |
|---|---|---|---|
| 刷新率 | 屏幕每秒刷新次数 | 60Hz / 90Hz / 120Hz | 硬件(屏幕) |
| 帧率 | 浏览器每秒生成的画面数 | 30fps / 60fps | 软件(渲染性能) |
刷新率是上限,帧率是实际值。
如果屏幕是 60Hz,浏览器每 16.67ms 需要生成一帧。如果生成一帧花了 30ms,帧率就掉到 33fps,你会感觉到卡顿。
为什么是 16.67ms?
1000ms ÷ 60 = 16.67ms
这就是 一帧的预算。在这 16.67ms 里,浏览器要完成 JS 执行、样式计算、布局、绘制、合成……任何一个阶段超时,都会掉帧。
我的经验:做性能优化时,我习惯用 Chrome DevTools 的 Performance 面板录一段操作,看每一帧的耗时。如果某帧超过 16.67ms,就放大看是哪个阶段超了。80% 的情况,问题出在 Layout 或 Paint 阶段。
2.5 本章小结
这一节我们拆解了浏览器渲染的五个阶段,理解了 GPU 加速的本质,也搞清了帧率和刷新率的关系。
记住三句话:
- 渲染流水线是分阶段的,不是所有操作都走全流程。
- GPU 加速的核心是跳过 Layout 和 Paint,只走合成。
- 16.67ms 是每一帧的死线,超了就卡。
这些基础概念,是后面所有优化手段的根基。你理解了渲染流水线,就知道什么时候该用 transform,什么时候该用 will-change,什么时候该减少 DOM 节点。