Skip to content
浏览器渲染流程
概述
浏览器将 HTML 字节流转换为屏幕像素的过程,是一条相对固定的流水线。理解每个阶段在做什么、什么操作会阻塞什么、成本集中在哪些环节,是分析前端性能的基础。
HTML 字节流
→ 解析(Parser)→ DOM 树
→ 解析(Parser)→ CSSOM 树
→ DOM + CSSOM → 渲染树(Render Tree)
→ 布局(Layout / 回流)→ 布局树(Layout Tree)
→ 绘制(Paint)→ 绘制图层(Paint Layers)
→ 合成(Composite)→ GPU 纹理 → 屏幕这些阶段并非严格的串行流程。例如,HTML 解析到 <img> 时,浏览器可以在继续构建 DOM 的同时启动图片下载;解析与网络加载允许交错执行。
HTML 解析与 DOM 树
主线程上的 HTML 解析器(HTMLDocumentParser)逐字节解析 HTML,构造 DOM 树。
外部资源标签(<img>、<link rel="stylesheet">)不会暂停解析器——资源加载由网络线程异步处理,DOM 构建继续进行。
默认的 <script> 标签会阻塞 DOM 解析。浏览器必须执行完脚本才能继续构建 DOM,因为脚本可能通过 document.write 修改正在处理的 HTML。这也是脚本通常被放置在 <body> 底部,或者声明为 defer / async 的原因。
预加载扫描器是一个轻量级组件,与 DOM 构建并行工作。它提前扫描 HTML 中后续的资源引用(图片、脚本、样式、字体),尽早发起网络请求。即使某个 <script> 阻塞了 DOM 构建,预加载扫描器仍能发现脚本后面的图片并开始下载——它不等待 DOM 构建完成。
脚本的下载与执行行为对 DOM 解析的影响:
| 脚本类型 | 下载时机 | 执行时机 | DOM 解析阻塞 | 执行顺序 |
|---|---|---|---|---|
<script> | 解析到标签时 | 下载完成后立即执行 | ✅ 阻塞 | 文档序 |
<script defer> | 异步下载 | DOM 解析完成后 | ❌ | 文档序 |
<script async> | 异步下载 | 下载完成后立即执行 | ❌ | 不保证 |
<script type="module"> | 异步下载 | 类似 defer | ❌ | 文档序 |
通常,业务脚本使用 defer 维持执行顺序;独立第三方脚本使用 async;需要尽早运行的关键内联脚本保留在 <head>。
CSS 解析与 CSSOM 树
CSS 解析生成 CSSOM(CSS Object Model)。
CSS 解析不阻塞 DOM 构建,但会阻塞渲染树的生成——渲染树必须合并 DOM 与 CSSOM,因此 CSS 属于渲染阻塞资源。在 CSSOM 就绪之前,浏览器不会开始渲染。
同时,CSS 解析也会阻塞后续脚本的执行。如果某个 <script> 之前存在尚未完成的样式表加载,脚本执行会被推迟到样式表加载完毕。原因在于脚本可能通过 getComputedStyle 查询元素的计算样式,此时样式必须反映最新状态。
CSS 解析本身速度通常很快。CSS 语法相对简单,解析耗时在微秒级,渲染阻塞主要来自网络下载时间,而非解析开销。
渲染树构建
DOM 树与 CSSOM 树合并形成渲染树。这一阶段主要完成三件事:
- 过滤不可见节点:
<head>、<script>以及设置了display: none的元素被排除; - 展开伪元素:
::before、::after作为独立的渲染节点插入; - 计算每个节点的最终样式:通过选择器匹配、级联与继承完成。
关于可见性:
display: none的元素不进入渲染树,不占用布局空间。visibility: hidden的元素会进入渲染树,因为它占据空间,只是不可见。opacity: 0的元素同样在渲染树中——透明度为零,但元素仍然参与布局和事件。
注意,渲染树与 DOM 树并非一一对应:部分 DOM 节点(如 <head>)不会产生渲染树节点,而伪元素(::before、::after)会在渲染树中生成独立的节点。
布局(回流)
布局阶段计算每个可见元素的几何信息(位置和尺寸)。在整个渲染管线中,这是成本最高的阶段之一。
父元素尺寸变化会引发子元素尺寸重新计算,进而影响兄弟元素的位置。这种连锁效应使布局成本与受影响元素的数量成正比,而非固定开销。
会触发布局的常见操作包括:
- 修改元素的
width/height/padding/margin/border - 修改
display/position/top/left/right/bottom - 添加或移除 DOM 节点
- 修改
font-size/font-family(影响文本尺寸) - 读取
offsetWidth/offsetHeight/getBoundingClientRect()等——如果当前布局信息已失效,会触发强制同步布局
强制同步布局(Forced Synchronous Layout)是一个高频性能问题:
js
// 触发强制同步布局:修改样式后立刻读取布局属性
element.style.width = '100px'; // 样式失效
const height = element.offsetHeight; // 强制立即布局以获取新值
// 避免:先批量读取,再批量写入
const height = element.offsetHeight; // 先读取(使用当前布局)
element.style.width = '100px'; // 再写入(下一次布局时生效)在循环中,这种行为的影响会被放大:
js
// 不良:每次迭代都触发强制同步布局
for (let i = 0; i < items.length; i++) {
items[i].style.width = items[i].offsetWidth + 10 + 'px';
}
// 良好:先收集所有旧值,再统一写入新宽度
const widths = items.map(item => item.offsetWidth);
for (let i = 0; i < items.length; i++) {
items[i].style.width = widths[i] + 10 + 'px';
}单次强制同步布局耗时通常在 0.5–5ms 之间(取决于 DOM 规模)。若循环执行 1000 次,两种写法的耗时差异可能达到 5 秒与 5 毫秒的级别。
绘制
绘制阶段将渲染树中的视觉样式(颜色、边框、阴影、背景等)填充为像素。
布局触发后必然触发绘制(几何变化 → 像素变化)。绘制也可以独立触发:修改 color / background-color / box-shadow 等仅影响视觉、不改变布局的属性时,只需重绘。
绘制的产物是绘制图层(Paint Layers)。浏览器根据 will-change、3D transform、<video>、<canvas> 等条件将某些元素提升为独立的合成层——这些元素可由 GPU 在合成线程上直接处理,从而避免主线程的绘制开销。
此外,一些叠加效果也可能导致隐式合成。例如应用了 filter、opacity 动画、overflow: scroll 的元素,以及与其他合成层发生重叠且 z-index 较低的元素,浏览器会将其提升到独立的合成层,以保证正确的绘制顺序和裁剪效果。
合成
合成阶段将绘制图层按 z-index 顺序合并为最终的屏幕图像。该步骤在合成线程上完成,不阻塞主线程。
transform 和 opacity 动画之所以开销低,是因为它们只触发合成,不触发布局与绘制。GPU 已在合成层中保留了元素的纹理,变换与透明度调整仅需在 GPU 上进行矩阵变换和 alpha 混合。
css
/* 只触发合成——可维持 60fps 的流畅动画 */
.element {
transform: translateX(0);
transition: transform 0.3s;
}
.element:hover {
transform: translateX(100px);
}
/* 触发布局 + 绘制 + 合成——帧率较低 */
.element {
left: 0;
transition: left 0.3s;
}
.element:hover {
left: 100px;
}如果提前知道某个元素将要发生合成层面的变化(如即将播放位移动画),可以使用 will-change 让浏览器预先创建合成层:
css
.element {
will-change: transform;
}需要注意,will-change 本身会占用 GPU 内存,不应全局滥用,通常只在动画即将开始前设置,结束后移除。
渲染阻塞关系
各类资源对 DOM 构建、渲染树生成和后续脚本执行的阻塞情况:
| 资源 | 阻塞 DOM 构建 | 阻塞渲染树 | 阻塞后续脚本 |
|---|---|---|---|
<link rel="stylesheet"> | ❌ | ✅ | ✅ |
<script>(无属性) | ✅ | — | — |
<script defer> | ❌ | — | — |
<script async> | ❌ | — | — |
<img> | ❌ | ❌ | ❌ |
@font-face | ❌ | 部分(取决于 font-display) | ❌ |
使用 Chrome DevTools 观测渲染管线
在 Chrome DevTools 的 Performance 面板中开始录制,可以捕获页面加载或交互期间的渲染事件:
- 蓝色:HTML 解析(Parse HTML)
- 紫色:布局(Layout / Reflow)
- 绿色:绘制(Paint)
- 灰色:合成层(Composite Layers)
红色三角标记表示强制同步布局(Forced Reflow),通常是性能分析的首要关注点。
页面调试中,性能问题的根因往往不是单次布局耗时过长,而是布局被触发的次数过多。在 Performance 面板中应优先关注布局事件的数量和频率,而非单次耗时。
