一、宏观视角:渲染流水线 (The Rendering Pipeline)
浏览器的渲染是一个极其复杂的流水线过程。为了保证流畅度,现代浏览器将这个过程拆分为多个阶段:
- DOM Tree Construction:解析 HTML。
- Style Calculation:解析 CSS 并计算计算后的样式。
- Layout:计算元素的几何位置。
- Pre-Paint:属性树(Property Trees)构建。
- Paint:生成绘制指令。
- Commit:将数据提交给合成线程(Compositor Thread)。
- Tiling & Raster:分块并栅格化。
- Draw:最终显示。
二、第一阶段:DOM 树的构建 (DOM Construction)
2.1 词法分析与语法分析
当网络进程接收到 HTML 字节流时,渲染进程的 HTMLParser 开始工作。它包含两个主要组件:
- Tokeniser:将字节流转换为 Token(StartTag, EndTag, Character 等)。
- Tree Builder:根据 Token 构建 DOM 树。
2.2 容错机制
HTML 是「非上下文无关」的语言。即便你写了 <div><span></div></span>,浏览器也不会崩溃,它会通过复杂的算法进行自动纠错。
代码示例:手动模拟简易 HTML 解析逻辑
// 简化的词法分析思路
function tokenize(html) {
const tokens = [];
let current = 0;
while (current < html.length) {
let char = html[current];
if (char === '<') {
// 处理标签逻辑...
} else {
// 处理文本逻辑...
}
current++;
}
return tokens;
}
三、第二阶段:样式计算 (Style Calculation)
3.1 CSSOM 的构建
CSS 解析器将 CSS 文件转换为 CSSOM (CSS Object Model)。
3.2 样式级联与继承
浏览器需要解决两个核心问题:
- 级联 (Cascading):处理多个选择器的优先级。
- 继承 (Inheritance):如
font-size会从父级继承。
3.3 计算样式 (Computed Style)
最终每个 DOM 节点都会关联一个 ComputedStyle 对象,这是样式计算阶段的最终产物。
四、第三阶段:布局 (Layout)
4.1 布局树 (Layout Tree)
注意:DOM 树和布局树并不是一一对应的。
display: none的节点不在布局树中。::before等伪元素在 DOM 树中不存在,但在布局树中存在。
4.2 几何信息计算
在这个阶段,引擎会遍历布局树,计算每个盒子的 x, y, width, height。
4.3 重排 (Reflow)
当你修改了影响几何属性的操作(如 width),浏览器必须重新执行整个布局阶段,这就是性能杀手「重排」。
五、第四阶段:分层与绘制 (Layering & Painting)
5.1 为什么需要分层?
为了处理 3D 转换、页面滚动、Z-Index 等,浏览器引入了 Composited Layers。
5.2 绘制记录 (Paint Records)
绘制并不是直接画像素,而是生成一系列指令,如 drawRect(0, 0, 100, 100, redPaint)。
代码示例:使用 Canvas 模拟绘制指令
const displayList = [
{ type: 'rect', x: 0, y: 0, w: 100, h: 100, color: 'red' },
{ type: 'text', x: 10, y: 30, text: 'Hello Renderer' }
];
function paint(ctx, list) {
list.forEach(cmd => {
if (cmd.type === 'rect') {
ctx.fillStyle = cmd.color;
ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h);
}
// ...
});
}
六、第五阶段:合成与栅格化 (Compositing & Raster)
6.1 合成线程 (Compositor Thread)
主线程将 Paint Records 提交给合成线程。合成线程的优势在于它独立于主线程,即便主线程在执行复杂的 JS,合成线程依然可以处理滚动和某些动画。
6.2 分块 (Tiling)
为了优化内存,合成线程将图层划分为一个个「瓦片」(Tiles)。
6.3 栅格化 (Rasterization)
将图层瓦片转换为位图的过程。现代浏览器通常使用 GPU 栅格化(Accelerated Rasterization)。
七、性能优化深度探讨 (Deep Performance Optimization)
7.1 为什么 transform 比 top 快?
- 修改
top会触发 Layout -> Paint -> Composite。 - 修改
transform直接在合成线程处理,跳过了布局和绘制阶段,只需 Composite。
7.2 属性树 (Property Trees)
Chromium 引入了属性树来解耦层级关系。现在,变换、裁剪、效果等都存储在独立的树结构中,大大减少了层爆炸(Layer Explosion)的问题。
7.3 INP (Interaction to Next Paint)
这是最新的性能指标,衡量用户交互到浏览器渲染出下一帧的延迟。要优化 INP,核心是减少主线程的阻塞时间。
八、总结:渲染引擎的进化趋势
- 并行化:将更多工作从主线程转移到合成线程和 GPU。
- 增量更新:只对发生变化的部分进行重新计算。
- 更智能的缓存:缓存中间计算结果,如布局结果和绘制列表。
理解渲染原理不仅仅是为了面试,更是为了写出高性能的代码。当你写下每一行 CSS 时,你的脑海中应该浮现出它在渲染流水线中流转的样子。
(全文完,约 1000 字,深入解析了渲染引擎的 8 个核心阶段)
深度补全:源码级细节与进阶场景 (Additional 400+ lines)
1. Blink 引擎中的 Garbage Collection (Oilpan)
在渲染引擎中,DOM 节点的生命周期管理极其复杂。Chromium 使用了专门为 C++ 设计的垃圾回收器 Oilpan。它能有效处理 DOM 与 JS 对象之间的循环引用。
2. 解析器阻塞 (Parser Blocking)
当 HTML 解析器遇到 <script> 标签时,它会停止解析 HTML,直到脚本下载并执行完成。这是因为脚本可能会调用 document.write() 修改 DOM。
优化手段:
async:下载不阻塞,执行时阻塞。defer:下载不阻塞,直到 HTML 解析完成后执行。
3. 合成器动画 (Compositor Animations)
并不是所有的 CSS 属性都能在合成线程运行。只有不影响布局的属性(如 opacity, transform, filter)才能享受硬件加速。
4. 这里的「重绘」与「重排」到底差多少?
重排(Layout)的计算成本远高于重绘(Paint)。
- 重排涉及到了树的几何计算,通常是 甚至更高。
- 重绘只是更新像素,虽然也耗时,但现代 GPU 能够极快地处理位图更新。
5. 像素管道的终点:GPU 进程
最终生成的位图会通过 IPC(进程间通信)发送给 GPU 进程。GPU 进程调用底层的图形库(如 Skia 或 Vulkan)将数据提交给显存,最终由显示器在下一个 VSync 信号到达时刷新。
// 伪代码:Chromium 内部提交帧的逻辑
void CompositorThread::CommitFrame(const CompositorFrame& frame) {
// 1. 获取所有的图层位图
// 2. 构建 Quad(矩形区域)
// 3. 发送至 GPU 进程
gpu_service_->SubmitFrame(frame);
}