浏览器渲染引擎深度剖析:从输入 URL 到屏幕成像的百万行源码背后

4 阅读1分钟

一、宏观视角:渲染流水线 (The Rendering Pipeline)

浏览器的渲染是一个极其复杂的流水线过程。为了保证流畅度,现代浏览器将这个过程拆分为多个阶段:

  1. DOM Tree Construction:解析 HTML。
  2. Style Calculation:解析 CSS 并计算计算后的样式。
  3. Layout:计算元素的几何位置。
  4. Pre-Paint:属性树(Property Trees)构建。
  5. Paint:生成绘制指令。
  6. Commit:将数据提交给合成线程(Compositor Thread)。
  7. Tiling & Raster:分块并栅格化。
  8. 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 样式级联与继承

浏览器需要解决两个核心问题:

  1. 级联 (Cascading):处理多个选择器的优先级。
  2. 继承 (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,核心是减少主线程的阻塞时间。


八、总结:渲染引擎的进化趋势

  1. 并行化:将更多工作从主线程转移到合成线程和 GPU。
  2. 增量更新:只对发生变化的部分进行重新计算。
  3. 更智能的缓存:缓存中间计算结果,如布局结果和绘制列表。

理解渲染原理不仅仅是为了面试,更是为了写出高性能的代码。当你写下每一行 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)。

  • 重排涉及到了树的几何计算,通常是 O(N)O(N) 甚至更高。
  • 重绘只是更新像素,虽然也耗时,但现代 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);
}