浏览器渲染页面
浏览器解析 HTML 和渲染页面是一个复杂但高度优化的流水线过程,称为 关键渲染路径。主要步骤分解如下:
🧱 1. 构建 DOM 树
- 输入: 原始 HTML 字节流。
- 过程:
- 字节 → 字符: 浏览器将字节解码为字符(根据文件编码,如 UTF-8)。
- 字符 → Tokens: 分词器将字符流分解为符合 HTML 标准的词法单元(Tokens),如
<html>
,<body>
,<div>
,"text"
等。 - Tokens → Nodes: 语法分析器(Parser)根据 HTML 规范(特别是树构建阶段)将这些 Tokens 解析成有层次结构的对象,即节点(Nodes)。每个节点代表 HTML 文档中的一个元素、属性、文本内容、注释等。
- Nodes → DOM Tree: 节点按照其在文档中的父子、兄弟关系连接起来,形成一颗树状结构——文档对象模型树。DOM 树是 HTML 文档在内存中的完整对象表示,也是 JavaScript 操作页面的主要接口。
- 特点:
- 渐进式: 浏览器不会等到整个 HTML 下载完才开始解析。它是边下载边解析的。
- 阻塞 JS/CSS: 遇到
<script>
标签(尤其是没有async
或defer
属性的)会暂停 HTML 解析,立即下载(如果需要)并执行脚本,因为 JS 可能修改 DOM。遇到<link>
引入的 CSS 会并行下载,但构建 CSSOM 会阻塞后续 JS 执行和渲染(见下一步)。
🎨 2. 构建 CSSOM 树
- 输入: 所有 CSS 来源(外部样式表、内联样式、
<style>
标签、浏览器默认样式)。 - 过程:
- 与 DOM 构建类似:字节 → 字符 → Tokens → Nodes。
- 节点根据 CSS 级联、继承和特异性规则连接成一棵树状结构——CSS 对象模型树。CSSOM 树包含了页面所有元素的最终计算样式(Computed Style)。
- 特点:
- 级联: 解决多个来源、选择器冲突的样式规则,确定最终值。
- 阻塞渲染: 浏览器必须拥有完整的 CSSOM 才能进行渲染树的构建(下一步)。因此,CSS 是渲染阻塞资源。优化 CSS 加载速度(内联关键 CSS、异步加载非关键 CSS)对首屏渲染性能至关重要。
- 阻塞 JS: 在 CSSOM 构建完成前,后续的 JS 执行(如果 JS 试图访问尚未解析的 CSS 属性)可能会被阻塞。
🌳 3. 构建渲染树
- 输入: DOM 树 + CSSOM 树。
- 过程:
- 遍历 DOM 树中的每一个可见节点。
- 对于每个可见节点,在 CSSOM 树中找到匹配的样式规则,应用这些规则。
- 将带有最终计算样式的可见节点组合成另一棵树——渲染树。
- 关键点:
- 仅包含可见内容: 渲染树只包含需要在屏幕上显示的元素。
display: none;
的元素、<head>
、<script>
等不会包含在内。visibility: hidden;
或opacity: 0;
的元素会保留在渲染树中(占据空间)。 - 每个节点包含视觉信息: 渲染树节点(通常称为渲染对象或渲染器)存储了元素的几何信息(位置、尺寸)和绘制指令。
- 仅包含可见内容: 渲染树只包含需要在屏幕上显示的元素。
📐 4. 布局
- 输入: 渲染树。
- 过程:
- 计算渲染树中每个节点在视口内的精确位置(x, y 坐标)和尺寸(宽度、高度)。这个过程也称为 重排。
- 布局是一个递归过程:通常从根节点(
<html>
)开始,遍历整个渲染树,根据 CSS 的盒模型、定位(普通流、浮动、绝对/固定定位)、弹性/网格布局等规则计算所有节点的几何信息。 - 输出是一个包含所有节点位置和尺寸信息的“盒子模型”。
- 特点:
- 相对单位转换: 将百分比、视口单位(vw/vh)等转换为屏幕上的实际像素值。
- 成本高昂: 改变元素的几何属性(宽、高、位置、边距等)会触发整个或部分渲染树的重新布局(重排),这是影响性能的主要操作之一。
🖌️ 5. 绘制
- 输入: 布局后的渲染树。
- 过程:
- 将渲染树分解成多个图层。某些元素(如
transform
,opacity
,position: fixed
)会提升到独立的合成层(Compositing Layer)。 - 为每个图层生成绘制指令列表。这些指令描述了如何将元素的各个部分(背景、边框、文本、图片等)绘制到屏幕上的具体像素位置。这类似于画家的草图或指令集。
- 这个过程也称为光栅化的准备工作。绘制指令本身不直接操作像素。
- 将渲染树分解成多个图层。某些元素(如
- 特点:
- 分层与合成: 分层使得浏览器可以对变化的部分进行高效的局部更新(只需重绘该层,然后与其他层合成)。这是现代浏览器实现流畅动画(如 transform, opacity)的关键。
- 重绘: 改变元素的视觉属性但不影响布局(如颜色、背景图、阴影)只会触发重绘(Repaint),相对重排开销较小。
🖥️ 6. 合成与显示
- 输入: 各图层的绘制指令。
- 过程:
- 光栅化: 浏览器的主线程或专门的合成线程/进程(取决于浏览器架构)将绘制指令转换为屏幕上的实际像素点。这通常发生在图块中(将图层划分为小块)。
- 合成: 合成线程将各个图层(尤其是独立合成层)的光栅化结果(图块)按照正确的顺序(z-index, 层叠上下文)和位置组合在一起。
- 显示: 合成后的最终图像(帧)被传送到 GPU,最终显示在屏幕上。
- 特点:
- GPU 加速: 合成和最终的图像显示通常由 GPU 处理,速度非常快。
- 独立线程: 现代浏览器将光栅化和合成工作放在与主线程(处理 JS、DOM、CSSOM、布局、绘制)不同的线程中,避免主线程繁忙(如 JS 执行)导致动画卡顿。
📌 关键点总结
- 渐进式处理: 浏览器尽力尽早显示内容,边下载、边解析、边渲染。
- 阻塞资源:
- CSS 是渲染阻塞: 浏览器会等待 CSSOM 构建完成才开始构建渲染树和布局。
- JS 是解析器阻塞: 同步 JS (
<script>
无async/defer
) 会阻塞 HTML 解析(从而阻塞 DOM 构建)和后续渲染。
- 渲染树是关键桥梁: 连接内容(DOM)和样式(CSSOM),只包含可见元素及其最终样式。
- 布局(重排)代价高: 影响几何属性的更改会触发整个或部分流程的重新计算(回流),应尽量避免或优化。
- 绘制(重绘)相对较轻: 只影响视觉样式不改变布局的操作只需重绘(可能还有合成)。
- 合成效率高: 利用分层和 GPU 加速进行合成,是实现高性能动画(transform, opacity)的基础。
- 事件循环: 整个渲染过程(以及 JS 执行)都是在浏览器的事件循环机制中调度执行的。渲染通常发生在事件循环的“渲染阶段”。
🚀 优化启示
- 最小化阻塞:
- 使用
async
/defer
加载非关键 JS。 - 内联关键 CSS,异步加载非关键 CSS。
- 避免在
<head>
中使用阻塞的 JS/CSS。
- 使用
- 减少 DOM 复杂度: 保持 DOM 结构简洁,减少节点数量。
- 高效 CSS 选择器: 避免过于复杂或低效的选择器(如深层嵌套、通配符
*
)。 - 优化 JS 执行: 避免长任务,使用
requestAnimationFrame
进行视觉变更。 - 减少重排/重绘:
- 批量 DOM 读写(使用虚拟 DOM 库或
DocumentFragment
)。 - 避免在循环中操作样式。
- 使用 CSS
transform
和opacity
进行动画(利用合成层)。 - 避免频繁读取会触发重排的属性(如
offsetTop
,scrollTop
,getComputedStyle
)。
- 批量 DOM 读写(使用虚拟 DOM 库或
- 利用合成层: 对动画元素使用
will-change: transform;
或transform: translateZ(0);
谨慎提升为合成层(有内存开销)。
理解这个流程对于诊断页面性能问题(使用浏览器 DevTools 的 Performance 面板)和进行有效的性能优化至关重要。