2. 渲染机制基础:浏览器渲染流水线

做界面移植和页面切换优化,绕不开一个核心问题:浏览器到底是怎么把代码变成像素的?

我见过不少同学,一上来就怼 CSS 动画、怼 transform,效果没出来,反而更卡了。说白了,你不懂渲染流水线,就不知道瓶颈在哪。

这一节,我们就把浏览器渲染的底牌翻出来看看。

2.1 从 URL 到像素:渲染流水线全景

浏览器拿到 HTML 和 CSS 后,不是一口气画出来的。它走了一条固定的流水线。我个人习惯把它拆成五个阶段:

  1. 构建 DOM 树 — 解析 HTML,生成节点树
  2. 构建 CSSOM 树 — 解析 CSS,生成样式规则树
  3. 构建渲染树 — 合并 DOM + CSSOM,只保留可见节点
  4. 布局(Layout) — 计算每个节点的几何位置和尺寸
  5. 绘制(Paint) — 把像素画到屏幕上

嗯,这里要注意:不是每次更新都会走完这五步。有些操作只触发后面几步,这就是优化的空间。

核心记忆点:渲染流水线是“增量”的。你改一个颜色,浏览器可能只走 Layout + Paint;你改 transform,它可能连 Layout 都跳过。

浏览器渲染流水线(五阶段) DOM 树 CSSOM 树 渲染树 布局 绘制 关键说明: • DOM 树 + CSSOM 树 → 渲染树(只包含可见节点,display:none 的节点被剔除) • 布局阶段计算每个节点的精确位置和尺寸(单位:像素) • 绘制阶段将布局结果转换为屏幕上的实际像素

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 节点。