浏览器渲染原理
前言
浏览器渲染原理是前端开发者的核心基础知识之一。理解从 HTML、CSS 代码到用户在屏幕上看到的像素这一完整过程,对于优化页面性能、排查渲染问题以及构建流畅的用户体验至关重要。本文将系统性地深入讲解浏览器渲染的每一个关键环节。
一、DOM Tree 构建过程
当浏览器通过网络接收 HTML 文档后,渲染引擎(如 Blink、WebKit)会开始解析 HTML 并构建 DOM(Document Object Model)树。这个过程可以分解为以下阶段:
1.1 字节(Bytes)→ 字符(Characters)
原始 HTML 内容以二进制字节流的形式传输,浏览器根据文件的编码格式(通常是 UTF-8)将这些字节解码为可识别的字符。
1.2 字符(Characters)→ Token(词法分析)
将字符序列输入到词法分析器(Lexer)中,按照 HTML 规范(如 <!DOCTYPE html>、标签、属性、注释等规则)进行分词。词法分析器会识别出起始标签(StartTag)、结束标签(EndTag)、自闭合标签、注释、DOCTYPE 等 Token 类型。
1.3 Token → 节点(Nodes)
Token 序列被送入树构建算法(Tree Construction Algorithm)中。此阶段根据 HTML 规范中的"插入模式"(Insertion Modes),将 Token 转换为具体的 DOM 节点。例如,遇到 <div> 起始标签 Token 时,会创建一个 HTMLDivElement 节点。
1.4 节点(Nodes)→ DOM Tree
节点按照 HTML 的嵌套结构组织成一棵树。浏览器维护一个"开放式元素栈"(Stack of Open Elements),当遇到起始标签时,创建节点并压入栈顶;遇到结束标签时,弹出栈顶元素,从而构建正确的父子层级关系。
关键特性:
- DOM 构建过程是渐进式的:浏览器可以在接收到全部 HTML 之前就开始构建 DOM,边下载边解析。
- 脚本(尤其是没有标记
async或defer的<script>标签)会阻塞 DOM 构建,因为脚本可能通过document.write()修改文档流。
二、CSSOM 构建过程
CSSOM(CSS Object Model)是 CSS 的树形结构化表示,构建过程与 DOM 类似:
2.1 解析 CSS 文本
浏览器收集所有 CSS 资源(包括外部样式表、<style> 标签内联样式以及元素的 style 属性),将其解析为一系列 CSS 规则。
2.2 词法与语法分析
与 HTML 类似,CSS 解析器会经历词法分析和语法分析阶段,将 CSS 文本分解为 Token,再根据 CSS 语法规则构建规则对象。每个规则对象包含选择器(Selector)和声明块(Declaration Block,即属性和值的映射)。
2.3 构建树形结构
CSSOM 之所以是树形结构,是因为 CSS 规则存在继承关系。例如,为 body 设置的 font-family 属性会继承给它的所有子元素,除非子元素显式覆盖。CSSOM 树与 DOM 树对应,但表示的是样式规则的层级关系。
重要特性:
- CSSOM 构建是阻塞渲染的:浏览器在 CSSOM 构建完成之前不会开始渲染,因为必须先知道所有元素的样式规则,才能进行后续的布局和绘制。
- CSS 的层叠(Cascade)和特异性(Specificity)计算在这一阶段完成。
三、Render Tree 合成
DOM 树描述了文档的结构,CSSOM 树描述了文档的样式,但两者并不能直接用于渲染。浏览器需要将 DOM 树和 CSSOM 树合并为渲染树(Render Tree)。
3.1 可见性计算
遍历 DOM 树中的每一个可见节点,并应用 CSSOM 中的样式规则。不可见的节点(如 <head> 及其子元素、设置了 display: none 的元素)不会出现在渲染树中。
注意:visibility: hidden 与 display: none 不同。前者元素不可见但仍占据空间,因此会出现在渲染树中;后者完全从渲染树中移除。
3.2 样式计算
对于渲染树中的每个节点,浏览器需要计算其最终样式(Computed Style)。这个过程包括:
- 应用所有匹配的 CSS 规则
- 按照层叠顺序和特异性合并规则
- 处理继承属性
- 将相对单位(如
em、rem、%)转换为绝对单位(如px)
每个渲染节点对应一个渲染对象(RenderObject),包含该节点的几何信息(宽高、位置)和绘制信息(颜色、背景、边框等)。
四、布局(Layout / Reflow)
布局阶段(也称为重排,Reflow)是浏览器计算每个渲染对象在视口中精确位置和大小的过程。
4.1 布局流程
- 遍历渲染树:从根渲染对象(
RenderView,对应视口)开始,按深度优先顺序遍历所有渲染对象。 - 计算几何信息:对每个渲染对象,根据其样式(宽度、高度、边距、填充、边框等)和父容器的约束条件计算其确切位置和尺寸。
- 确定坐标系:每个元素的位置相对于其包含块(Containing Block)确定。
- 处理溢出与换行:对于文本内容,需要计算行框(Line Box)的换行位置;对于溢出内容,决定是否显示滚动条。
4.2 触发条件
以下操作会触发重排:
| 操作 | 示例 |
|---|---|
| 改变元素几何属性 | width、height、padding、margin、border |
| 改变元素位置 | top、left、position、float |
| 改变窗口尺寸 | resize 事件 |
| 修改内容 | 文本变更、图片加载完成 |
| 添加/移除 DOM 节点 | appendChild、removeChild |
| 激活 CSS 伪类 | :hover 改变元素尺寸 |
| 查询布局信息 | offsetHeight、getBoundingClientRect() 等 |
布局抖动(Layout Thrashing):在短时间内连续读取并写入布局属性会导致多次强制重排,严重影响性能。避免方式是批量读取或使用 requestAnimationFrame 隔离读写操作。
五、绘制(Paint)
布局完成后,浏览器进入绘制阶段,将每个渲染对象转换为屏幕上的实际像素。
5.1 绘制流程
绘制阶段将渲染树中的每个节点绘制到多个图层(Layer)上。绘制顺序遵循 CSS 2.1 规范定义的层叠上下文(Stacking Context)规则:
- 元素的背景和边框
- 负
z-index的定位元素 - 块级盒子(Block Boxes)
- 浮动元素(Floats)
- 内联元素(Inline)和文本
- 正
z-index的定位元素
5.2 绘制记录
浏览器不会立即将绘制操作输出到屏幕上,而是生成一系列绘制记录(Paint Records),记录绘制指令的列表(如"在位置 (x, y) 绘制一个 100x50 的红色矩形")。这些记录会在后续的光栅化阶段被执行。
六、合成层(Composite Layers)与 GPU 加速
合成(Compositing)是现代浏览器提升渲染性能的关键技术。
6.1 什么是合成层
渲染树中的某些元素会被提升到独立的合成层(Composite Layer)中。每个合成层拥有独立的图形纹理(Texture),并由 GPU 负责合成。
6.2 哪些元素会创建合成层
- 具有 3D CSS 变换(如
transform: translateZ(0)、transform: rotateX(...)) - 使用
will-change属性的元素 - 使用
opacity动画的元素 <video>、<canvas>、<iframe>元素- 具有
overflow: scroll且内容溢出的元素 - 设置了
position: fixed的元素(在某些浏览器中)
6.3 GPU 加速原理
合成层由 GPU 单独处理为纹理,然后由合成器线程(Compositor Thread)将这些纹理在 GPU 上进行合成。由于 GPU 对纹理的变换(位移、旋转、缩放、透明度变化)非常高效,因此通过 transform 和 opacity 实现的动画不会触发重排和重绘,而是在合成线程中完成,性能极高。
核心优势:合成操作在合成器线程上执行,不占用主线程(Main Thread),因此不会受到 JavaScript 计算或样式计算的影响。
七、重排(Reflow)vs 重绘(Repaint)
对比例表:
| 对比维度 | 重排(Reflow) | 重绘(Repaint) |
|---|---|---|
| 定义 | 重新计算元素几何信息(位置和尺寸) | 重新绘制元素像素外观 |
| 触发条件 | 布局相关的属性变更 | 外观相关但不影响布局的属性变更 |
| 影响范围 | 可能影响子元素、相邻元素乃至整个文档 | 仅影响该元素所在区域 |
| 性能开销 | 高(涉及布局计算和后续绘制) | 中(仅绘制,不涉及布局) |
| 典型属性 | width、height、margin、padding、top、left | color、background-color、visibility、outline |
| 是否必然导致对方 | 重排必然导致重绘 | 重绘不会导致重排 |
优化策略:
- 使用
transform替代top/left做位移动画 - 使用
opacity替代visibility做显隐动画 - 批量修改 DOM 样式(使用
classList或 CSScontain属性) - 对频繁动画的元素使用
will-change或提升为合成层
八、影响渲染性能的关键因素
8.1 JavaScript 执行
JavaScript 运行在主线程上,长时间的 JS 执行会阻塞渲染。以下情况会严重影响性能:
- 大型循环或复杂计算
- 频繁的 DOM 操作导致布局抖动
- 未使用
requestAnimationFrame的动画逻辑 - 未优化的时间处理器(如高频的
scroll、resize事件)
8.2 样式计算复杂性
选择器的复杂性会影响样式计算性能。虽然现代浏览器的选择器匹配非常快,但在大型 DOM 树中使用过于复杂的选择器(如深层嵌套的选择器或通配选择器)仍可能产生可测量的开销。
8.3 布局复杂度
- 页面中元素数量越多,布局计算越耗时
- 使用表格布局(
display: table)时,布局计算更复杂 - 弹性布局(Flexbox)和网格布局(Grid)在子元素数量很大时也需注意性能
8.4 绘制复杂度
- 阴影(
box-shadow)、渐变(gradient)、圆角(border-radius)等视觉效果会增加绘制时间 - 大面积的绘制区域(如全屏背景图)会增加 GPU 纹理的内存占用
8.5 内存与 GPU 限制
- 过多的合成层会增加 GPU 内存消耗
- 对于移动设备,GPU 性能和内存带宽有限,过度使用合成层可能适得其反
九、关键渲染路径(Critical Rendering Path)优化
关键渲染路径(CRP)是指浏览器从接收 HTML 到完成首次渲染所经过的一系列步骤。优化 CRP 可以显著提升页面加载性能。
9.1 优化步骤
- 减少关键资源数量:阻塞渲染的资源越少,首次渲染越快。将非关键的 CSS 和 JavaScript 标记为非阻塞。
- 减少关键字节数:压缩 HTML、CSS 和 JavaScript 文件,移除不必要的注释和空格。
- 减少关键路径长度:优化资源加载顺序,让关键资源尽早开始加载。
9.2 具体优化策略
CSS 优化:
- 内联关键 CSS(Critical CSS),将首屏所需的样式直接嵌入 HTML 的
<head>中 - 使用
<link rel="preload">预加载关键资源 - 异步加载非关键 CSS(通过
media属性或loadCSS方法) - 避免使用
@import,它会串行阻塞 CSS 加载
JavaScript 优化:
- 使用
async或defer属性加载脚本,避免阻塞 DOM 构建 - 将非关键脚本延迟到首屏渲染之后加载
- 使用代码分割(Code Splitting)按需加载 JavaScript
HTML 优化:
- 尽早发送 HTML 首字节(TTFB 优化)
- 使用流式解析(Server-Sent HTML Streaming)
- 避免在
<head>中放置过多的同步脚本
9.3 CRP 性能指标
- FP(First Paint):首次绘制时间
- FCP(First Contentful Paint):首次内容绘制时间
- LCP(Largest Contentful Paint):最大内容绘制时间
- TTI(Time to Interactive):可交互时间
十、requestAnimationFrame 与渲染帧生命周期
10.1 渲染帧生命周期
浏览器通常以 60fps(帧率,即每秒 60 帧)为目标,这意味着每帧的可用时间约为 16.67ms。一帧的生命周期包括以下阶段:
- 输入事件处理:处理用户交互(触摸、点击、滚动等)
- JavaScript 执行:执行
requestAnimationFrame回调、定时器回调、事件处理器等 - 样式计算:重新计算元素样式
- 布局计算:执行重排
- 绘制:生成绘制记录
- 合成:将各合成层合成最终图像
- 提交帧:将合成后的帧提交到屏幕显示
10.2 requestAnimationFrame 的工作原理
requestAnimationFrame(简称 rAF)是浏览器提供的 API,用于在下一次重绘之前执行动画更新:
function animate(timestamp) {
element.style.transform = `translateX(${Math.sin(timestamp / 1000) * 100}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);与 setTimeout/setInterval 的对比:
| 对比维度 | requestAnimationFrame | setTimeout / setInterval |
|---|---|---|
| 执行时机 | 在浏览器下次重绘之前执行 | 在指定延迟后执行(不关心渲染周期) |
| 帧同步 | 与显示器刷新率同步 | 不与刷新率同步 |
| 后台标签页 | 页面不可见时暂停执行 | 继续执行(尽管有最小时间限制) |
| 性能 | 由浏览器优化,一次 rAF 回调合并到一帧中 | 可能造成多余的帧或丢帧 |
| 节流 | 自动适配刷新率(60Hz/120Hz/144Hz) | 需要手动处理 |
10.3 使用 rAF 的最佳实践
- 避免在 rAF 回调中执行耗时操作:如果 JavaScript 执行时间超过 16.67ms,会导致帧丢失(Frame Drop),表现为页面卡顿。
- 使用两个 rAF 回调:对于需要密集计算的任务,可以在一个 rAF 中读取布局信息,另一个 rAF 中写入新值,避免布局抖动。
- 利用帧时间戳:rAF 回调接收的
timestamp参数是高精度时间戳,用于计算时间增量,而不是依赖Date.now()。
10.4 渲染帧的观察工具
Chrome DevTools 的 Performance 面板可以可视化每一帧的生命周期,清晰展示 JavaScript 执行、样式计算、布局、绘制和合成各阶段的耗时,帮助定位渲染瓶颈。
总结
浏览器渲染是一个精密的管道流程:从 HTML/CSS 解析开始,经过 DOM Tree 和 CSSOM 的构建,合并为 Render Tree,再经过布局、绘制和合成,最终呈现为屏幕上的像素。理解这一过程中的每一个环节,可以帮助我们在开发中有针对性地进行性能优化,构建流畅高效的用户界面。
关键要点回顾:
- DOM 构建是渐进式的,脚本会阻塞解析
- CSSOM 构建会阻塞渲染
- Render Tree 仅包含可见节点
- 布局计算几何信息,触发成本最高
- 绘制按层叠上下文顺序生成绘制指令
- 合成层利用 GPU 加速,是高性能动画的关键
- CRP 优化需要关注资源数量、大小和路径长度
- requestAnimationFrame 是浏览器原生支持的动画调度机制