参考:
- Chromium ↗
- Chromium Docs ↗
- Inside look at modern web browser ↗
- Life of a Pixel ↗
- Chromium 渲染流水线——字节码到像素的一生 ↗
Inside look at modern web browser#
Architecture#
浏览器没有唯一的架构标准。以 Chrome / Chromium 为例,整体上由 Browser Process 协调多个子进程,例如 Renderer Process、GPU Process、Utility Process、Plugin Process 等。
- Browser Process:负责浏览器级能力,例如地址栏、书签、前进 / 后退按钮、标签页管理、权限、安全策略、导航协调等。网络请求通常由 Network Service 处理,它可能运行在独立的 Utility Process 中,而不是简单地理解为 Browser Process 内的一个网络线程。
- Renderer Process:负责 Web 内容的解析、执行、布局、绘制和合成调度。Renderer 会把页面内容转换为一系列绘制数据,最终提交为
viz::CompositorFrame。 - GPU Process / Viz:隔离 GPU 相关任务。Viz 负责跨进程的 Surface Aggregation、Display Compositor,以及最终通过 GPU API 上屏。
- Plugin Process:运行插件相关逻辑。现代 Chrome 中传统插件能力已经大幅减少,例如 Flash 已被移除。
多进程架构#
在多标签页场景中,Chrome 通常会创建多个 Renderer Process。更准确地说,Renderer Process 并不总是“一个标签页一个进程”,而是与 Site Instance / Browsing Instance / Site Isolation 等策略相关。
多进程的好处:
- 稳定性:一个页面崩溃通常不会影响整个浏览器。
- 安全性:Renderer 被沙盒化,降低恶意页面直接访问系统资源的风险。
- 隔离性:不同站点可以运行在不同进程中,减少跨站点内存共享带来的风险。
代价:
- 进程数量增加会带来更高的内存开销。
- 在低内存设备上,Chrome 可能复用 Renderer Process 或合并部分服务进程,以降低资源消耗。
服务化与内存节省#
Chrome 将很多浏览器能力拆成 Service,例如 Network Service、Audio Service、Storage Service 等。服务化后,Chrome 可以根据设备能力灵活决定服务运行方式:
- 在高性能设备上,不同服务可以运行在不同进程中,提高隔离性和稳定性。
- 在资源受限设备上,多个服务可以合并到同一进程中,以节省内存。
Site Isolation#
早期浏览器中,一个标签页内的内容,包括来自不同站点的 iframe,可能都运行在同一个 Renderer Process 中。虽然同源策略会限制脚本层面的访问,但不同站点仍然共享同一进程的地址空间,安全性不足。
现代 Chrome 引入 Site Isolation。在开启站点隔离后,跨站点 iframe 可以运行在独立的 Renderer Process 中。例如,一个页面来自 a.com,其中嵌入了 b.com 的 iframe,那么 a.com 和 b.com 可能分别运行在不同的 Renderer Process 中。
导航流程#
一次页面导航大致包括以下阶段:
- 处理输入:用户在地址栏输入内容后,Browser Process 的 UI 逻辑判断输入是搜索词还是 URL。
- 发起导航请求:Browser Process 协调导航,并通过 Network Service 发起网络请求。如果遇到重定向,Network Service 会把重定向结果通知导航逻辑,再继续请求新的 URL。
- 检查响应:响应到达后,浏览器会根据
Content-Type、MIME sniffing、安全策略、下载策略等判断后续处理方式。如果是 HTML 文档,数据会交给 Renderer Process;如果是下载类型,则进入下载流程。 - 选择或创建 Renderer Process:Browser Process 会根据当前站点、Site Isolation 策略、进程复用策略等,选择已有 Renderer Process 或创建新的 Renderer Process。
- 提交导航:Browser Process 通过 IPC / Mojo 通知 Renderer Process 提交导航。提交后,地址栏、安全状态、会话历史等浏览器 UI 状态也会随之更新。
- 加载并渲染页面:Renderer Process 继续解析 HTML、加载子资源、执行脚本、计算样式、布局、绘制、合成并提交帧。页面完成加载后,Renderer 会通知 Browser Process,浏览器 UI 停止显示加载状态。
- 离开页面检查:当用户离开当前页面或关闭标签页时,Browser Process / Renderer Process 会协作检查页面是否注册了
beforeunload事件。如果注册了,可能弹出“是否离开此网站”的提示。 - Service Worker:如果目标 URL 命中已注册的 Service Worker scope,导航请求可能先交给 Service Worker 处理。Service Worker 可以从缓存返回数据,也可以继续发起网络请求。
- 预加载:在导航过程中,浏览器可能通过 preload scanner、prefetch、prerender 等机制提前加载资源,以降低后续页面展示延迟。
渲染程序#
Renderer Process 的核心目标是把 HTML、CSS、JavaScript 转换成用户可以看到并交互的页面。
主要阶段:
- 构建 DOM:Renderer 接收 HTML 数据后,HTML Parser 将字节流解析为 DOM Tree。
- 加载子资源:解析过程中遇到图片、CSS、JavaScript、字体等资源时,会发起子资源请求。为了提高效率,预加载扫描器会并发发现和请求部分资源。
- 执行 JavaScript:HTML Parser 遇到普通同步
<script>时会暂停解析,因为 JavaScript 可能通过document.write或 DOM API 改变文档结构。可以使用async或defer减少脚本对 HTML 解析的阻塞。 - 样式计算:Blink 根据 CSS 规则和 DOM 节点计算每个元素的 Computed Style。
- 布局:Blink 根据 DOM、Computed Style、可用空间等信息构建和更新 Layout Tree,并计算元素的几何信息,例如位置和尺寸。
- 绘制:Blink 根据布局结果和样式生成绘制指令,形成 Display List / PaintArtifact。
- 合成与光栅化:Compositor 将页面拆分为适合独立更新的图层和图块,Raster 线程把绘制指令光栅化为像素资源。
- 提交与上屏:Renderer 提交
viz::CompositorFrame,Viz 聚合多个 Surface 后,由 Display Compositor 通过 GPU 输出到屏幕。
光栅化与合成#
光栅化是把矢量式的绘制指令转换为像素的过程。
一种最简单的渲染方式是:只对当前视口内的内容进行光栅化,用户滚动时移动已有像素,并对新出现的区域继续光栅化。但这种方式在复杂页面中容易产生卡顿。
现代浏览器通常使用 Compositing:
- 将页面拆成多个可独立更新的图层。
- 每个图层可以进一步拆成多个 Tile。
- Compositor 优先调度视口附近 Tile 的 Raster 任务。
- 滚动、transform、opacity 等某些变化可以只在 Compositor Thread 上处理,不必重新执行完整的 JavaScript、Style、Layout 和 Paint。
需要注意:并不是每个 DOM 元素都会成为独立图层。图层数量过多会增加内存、调度和合成成本。Chromium 会根据属性、动画、滚动、视频、canvas、transform、opacity 等因素决定是否提升为单独的 composited layer。
Rendering Pipeline#
浏览器内核与 Chromium 组成#
现代浏览器通常可以粗略拆成“浏览器内核 + 浏览器服务 / 产品集成”:
- Safari = WebKit + Apple 平台集成与服务
- Chrome = Chromium + Google 服务集成
- Microsoft Edge = Chromium + Microsoft 服务集成
浏览器内核也可以进一步拆分:
- 渲染引擎:负责 HTML / CSS 解析、样式、布局、绘制、合成等。Chromium 使用 Blink。
- JavaScript 引擎:负责 JavaScript 编译和执行。Chromium 使用 V8。
- 其他基础设施:网络、存储、多媒体、字体、输入、安全、IPC、GPU、调度等。
常见内核 / 引擎关系:
- IE:Trident / MSHTML + JScript / Chakra
- Safari:WebKit + JavaScriptCore
- Chromium:Blink + V8 + Chromium 其他组件
Chromium 进程模型#
-
Browser Process
- 负责浏览器级 UI 和权限、安全、导航、标签页管理等能力。
- 不直接负责网页内容区域的 DOM / CSS / Layout / Paint。
-
Renderer Process
- 负责 Web 内容的解析、脚本执行、样式、布局、绘制、输入事件、滚动和动画调度等。
- Main Thread:执行 JavaScript、HTML parsing、Style、Layout、Paint、事件分发等。
- Compositor Thread:负责合成调度、滚动、部分动画、生成 CompositorFrame 等。
- Raster Worker Threads:执行 Tile Raster 任务,把绘制指令光栅化为 GPU / Skia 资源。
- Worker Threads:运行 Web Worker、Service Worker 等。
-
GPU Process / Viz
- 负责 GPU 相关操作。
- Viz 接收 Renderer、Browser UI、视频等来源提交的
viz::CompositorFrame,进行 Surface Aggregation,并最终通过 Display Compositor 上屏。
-
Plugin Process
- 运行插件相关逻辑。现代 Chromium 中传统插件使用已经很少。
-
Utility Process
- 承载 Network Service、Audio Service、Storage Service、解码器等相对独立的服务。
流水线阶段#
1. Parsing#
- 模块:Blink
- 进程:Renderer Process
- 线程:Main Thread
- 职责:解析从网络层传入的 HTML 字节流,构建 DOM Tree。
- 输入:HTML bytes
- 输出:DOM Tree
数据流大致为:
bytes → characters → tokens → nodes → DOM TreetextLoading#
Blink 从网络层接收文档数据,典型链路可以粗略理解为:
Browser / Network Service
→ Renderer Process
→ blink::DocumentLoader
→ blink::HTMLDocumentParsertextConversion#
HTML Parser 根据编码信息把 bytes 转换为 characters。
例如:
0x3C 0x64 0x69 0x76 0x3Etext转换后:
<div>htmlTokenizing#
Parser 将字符流转换为 HTML Token。
例如:
<div>Hello</div>html转换后:
StartTag(div)
Text(Hello)
EndTag(div)text需要注意:解析过程中遇到 <link>、<script>、<img> 等标签时,可能触发子资源加载;遇到普通同步 <script> 时,HTML 解析可能暂停,直到脚本加载并执行完成。
Tree Construction#
HTML Tree Builder 根据 Token 创建 DOM 节点,并插入到 DOM Tree 中。
2. Style#
- 模块:Blink
- 进程:Renderer Process
- 线程:Main Thread
- 职责:根据 DOM、CSS 规则、继承、层叠、选择器匹配等计算每个元素的 Computed Style。
- 输入:DOM Tree + CSS Rules
- 输出:Computed Style
CSS 解析与 HTML 解析类似,也会经历字节流解析、Tokenize、构建规则等阶段。Style Engine 会将 CSS 规则组织成便于快速匹配的数据结构,然后对 DOM 节点进行样式匹配和样式计算。
这里不建议简单说 Style 阶段输出 Render Tree。更准确地说,现代 Blink 中 Style 阶段主要输出每个元素的 Computed Style,之后 Layout 阶段基于 DOM 和 Computed Style 构建 / 更新 Layout Tree。
3. Layout#
- 模块:Blink
- 进程:Renderer Process
- 线程:Main Thread
- 职责:计算元素的几何信息,例如位置、尺寸、行盒、滚动区域等。
- 输入:DOM Tree + Computed Style
- 输出:Layout Tree / Layout Objects with geometry
Layout 的大致过程:
- 根据 DOM 和 Computed Style 创建或更新 Layout Object。
- 从根节点开始布局。
- 父元素向子元素传递约束,例如可用宽度、格式化上下文等。
- 子元素根据约束计算自身尺寸。
- 父元素根据子元素结果确定自身尺寸。
- 记录每个 Layout Object 的位置、大小和相关几何信息。
4. Pre-paint#
- 模块:Blink
- 进程:Renderer Process
- 线程:Main Thread
- 职责:为后续 Paint 和 Compositing 准备绘制属性、失效区域和 Property Trees。
- 输入:Layout Tree / Layout Objects
- 输出:Paint Property Trees、Paint invalidation 信息等
Layout 已经计算出元素的位置和尺寸。Pre-paint 进一步判断这些元素在绘制和合成时需要哪些附加属性,例如 transform、clip、effect、scroll,并生成 / 更新 Paint Property Trees。
常见 Property Trees:
- Transform Tree:表达 transform、坐标空间变化。
- Clip Tree:表达裁剪关系。
- Effect Tree:表达 opacity、filter、blend mode 等效果。
- Scroll Tree:表达滚动节点及滚动关系。
问题:生成的 Property Trees 是否完整复制 Layout Tree?
不是。Paint Property Trees 通常是稀疏树,不会为 Layout Tree 的每个节点都创建对应节点。只有当某个对象引入新的 transform、clip、effect、scroll 等绘制属性状态时,才需要创建新的 Property Tree 节点。
5. Paint#
- 模块:Blink
- 进程:Renderer Process
- 线程:Main Thread
- 职责:根据 Layout 结果和 Paint Property Trees 生成绘制指令。
- 输入:Layout Objects + Computed Style + Paint Property Trees
- 输出:PaintArtifact
Paint 的大致过程:
- 遍历需要绘制的 Layout Object。
- 根据 Computed Style 和几何信息生成绘制指令。
- 每条绘制指令关联对应的 Property Tree State。
- 按绘制顺序形成 Display Item 列表。
- 按属性状态和绘制边界切分为 Paint Chunks。
- 最终形成 PaintArtifact。
输出结构可以粗略理解为:
PaintArtifact
├── DisplayItemList
│ ├── DisplayItem 0
│ ├── DisplayItem 1
│ ├── DisplayItem 2
│ └── ...
└── PaintChunk list
├── PaintChunk 0 → 引用 DisplayItemList 的一段范围
├── PaintChunk 1 → 引用 DisplayItemList 的一段范围
└── ...text说明:
- Display List / DisplayItemList:所有绘制项的线性列表,按绘制顺序排列。
- Paint Chunk:Display List 中共享相同或相近 Property Tree State 的一段绘制内容。
- PaintArtifact:Blink Paint 阶段的核心产物,包含 Display Items 和 Paint Chunks。
6. Compositing / Layerization#
- 模块:Blink + cc
- 进程:Renderer Process
- 线程:Main Thread,随后提交到 Compositor Thread
- 职责:根据 PaintArtifact、Property Trees、滚动、动画、视频、canvas 等因素决定哪些内容需要独立合成,并生成 cc 使用的数据结构。
- 输入:PaintArtifact + Paint Property Trees
- 输出:cc Layer tree / Property Trees / Layer list 等提交数据
Compositing 的核心目标是把页面拆成适合独立更新的合成单元。这样在某些场景下,例如滚动、transform 动画、opacity 动画、视频播放等,可以避免重新执行完整的 Style、Layout 和 Paint。
分层大致可以理解为:
- Pre-paint 生成 Paint Property Trees。
- Paint 生成 PaintArtifact,其中包含 Display Items 和 Paint Chunks。
- PaintArtifactCompositor 根据 Paint Chunks、Property Tree State、合成原因等进行 layerization。
- 生成或更新 cc 侧需要的 Layer / Property Tree 数据。
需要注意:旧资料里经常会提到 GraphicsLayer、PaintLayer、cc::Layer。这些概念在不同 Chromium 版本中含义和使用方式会变化。阅读源码时需要结合当前版本,而不是把它们简单等同为“Paint 的输出”。
7. Commit#
- 模块:cc
- 进程:Renderer Process
- 线程:Main Thread → Compositor Thread
- 职责:把 Main Thread 上构建好的 Layer Tree、Property Trees、Paint records 等提交给 Compositor Thread。
- 输入:Main Thread 侧的 cc 数据结构
- 输出:Compositor Thread / Impl Thread 侧的 LayerImpl Tree
Commit 可以理解为 Main Thread 到 Compositor Thread 的一次数据同步。提交后,Compositor Thread 拥有可用于调度 Raster、Activate 和 Draw 的 impl-side 数据结构。
8. Tiling#
- 模块:cc
- 进程:Renderer Process
- 线程:Compositor Thread
- 职责:将需要光栅化的图层内容按 scale 和可见区域拆成多个 Tile,并生成 Raster 任务。
- 输入:LayerImpl / PictureLayerImpl
- 输出:Raster Tasks / Tile Tasks
Tile 是 Raster 的基本工作单位。Commit 完成后,Compositor 会根据视口、滚动方向、缩放比例、优先级等信息,为需要的 Tile 创建 Raster 任务。
这样可以优先光栅化用户当前能看到或即将看到的区域。
9. Raster#
- 模块:cc + Skia + GPU Raster 相关组件
- 进程:Renderer Process,部分资源和 GPU 操作涉及 GPU Process
- 线程:Raster Worker Threads
- 职责:执行 Tile Raster 任务,把绘制记录转换为可被合成使用的像素资源。
- 输入:Raster Tasks / Paint records / Tile 信息
- 输出:Tile 对应的 GPU / Skia 资源
Raster 阶段不是直接把内容写入 viz::CompositorFrame。它主要负责为 Tile 生成可供后续 Draw 阶段引用的资源,例如 GPU texture 或 software bitmap。
10. Activate#
- 模块:cc
- 进程:Renderer Process
- 线程:Compositor Thread
- 职责:在 Pending Tree 和 Active Tree 之间切换,确保 Draw 使用的是已经准备好的资源。
- 输入:Pending Tree
- 输出:Active Tree
cc 通常维护 Pending Tree 和 Active Tree。Pending Tree 承接新提交的数据和未完成的 Raster 资源;当满足激活条件后,Pending Tree 会被激活为 Active Tree。Draw 阶段使用 Active Tree 生成最终的 CompositorFrame。
11. Draw#
- 模块:cc / viz
- 进程:Renderer Process
- 线程:Compositor Thread
- 职责:遍历 Active Tree,把可见图层内容转换为
viz::DrawQuad,并组织成viz::CompositorFrame。 - 输入:Active LayerImpl Tree + Tile resources
- 输出:
viz::CompositorFrame
Draw 阶段本身不执行光栅化。它会遍历 Active Tree 中的 cc::LayerImpl,调用类似 AppendQuads 的逻辑,生成不同类型的 viz::DrawQuad,并放入 CompositorFrame 的 RenderPass 中。
常见 DrawQuad 类型包括:
TextureDrawQuadTileDrawQuadSolidColorDrawQuadRenderPassDrawQuadYUVVideoDrawQuad
12. Aggregate#
- 模块:Viz
- 进程:GPU Process / Viz Service
- 线程:Display Compositor 相关线程
- 职责:接收多个来源提交的
viz::CompositorFrame,对多个 Surface 进行聚合。 - 输入:多个 Surface /
viz::CompositorFrame - 输出:聚合后的 CompositorFrame / RenderPass 列表
Renderer Process 会提交自己的 CompositorFrame。Browser UI、iframe、视频、插件等也可能有各自的 Surface。Viz 的 Surface Aggregator 会把这些 Surface 聚合成最终用于显示的结构。
13. Display#
- 模块:Viz / GPU
- 进程:GPU Process
- 线程:Display Compositor / GPU 相关线程
- 职责:将聚合后的 RenderPass / DrawQuads 通过 GPU API 输出到屏幕。
- 输入:聚合后的 CompositorFrame / RenderPass / DrawQuads
- 输出:屏幕上的 pixels
Display 阶段会根据平台和配置使用 OpenGL、Vulkan、Dawn / WebGPU 相关后端、SkiaRenderer 等路径,把最终合成结果提交给窗口系统并显示到屏幕。
简化版数据流#
HTML / CSS / JS
→ DOM Tree
→ Computed Style
→ Layout Tree
→ Paint Property Trees
→ PaintArtifact
→ cc Layer / Property Trees
→ LayerImpl Tree
→ Tiles
→ Raster resources
→ viz::DrawQuad
→ viz::CompositorFrame
→ Surface Aggregation
→ Display
→ pixelstext需要特别区分的几个概念#
Paint 与 Raster#
- Paint:生成绘制指令,例如画文字、画矩形、画图片。产物偏“记录 / 指令”。
- Raster:执行绘制指令,把它们转换为像素资源。产物偏“bitmap / texture”。
Compositing 与 Raster#
- Compositing:决定哪些内容分层、如何合成、哪些变化可以只在 Compositor Thread 处理。
- Raster:把某个 Tile 的绘制内容真正转换成像素。
Draw 与 Display#
- Draw:Renderer 的 Compositor Thread 根据 Active Tree 生成
viz::CompositorFrame。 - Display:Viz / GPU Process 聚合多个 Surface,并把最终结果输出到屏幕。
Main Thread 与 Compositor Thread#
- Main Thread:负责 JavaScript、Style、Layout、Paint 等重任务。
- Compositor Thread:负责滚动、部分动画、Commit 后的调度、Tile 管理、Activate、Draw 等。
Compositor Thread 的价值在于:当页面某些变化不需要重新执行 JavaScript、Style、Layout、Paint 时,可以更快生成新帧,减少卡顿。
Chromium Docs: RenderingNG#
RenderingNG 不是一个单独模块,而是一组围绕 Chromium 渲染架构重构的长期项目。
它的核心目标是:浏览器引擎本身不应该成为 Web 体验的限制。开发者不应该因为浏览器 bug、性能断崖或缺少基础能力,被迫绕开标准 Web Platform。
RenderingNG 追求三类能力:
- Reliability:渲染结果可靠、正确,减少历史渲染 bug。
- Performance:性能可预测,避免复杂页面出现非线性性能退化。
- Extensibility:支持更多高级 Web 能力,例如复杂动画、滚动、视频、canvas、container queries 等。
官方用一个金字塔描述 RenderingNG 的建设顺序:
Extensibility
Performance
Reliabilitytext也就是说,先保证正确性和可靠性,再保证性能,最后才有能力稳定地扩展更多 Web Platform 特性。
RenderingNG 涉及多个子项目,包括:
- BlinkNG
- LayoutNG
- Composite After Paint
- GPU acceleration everywhere
- Threaded scrolling
- Threaded animations
- Threaded image decode
- Viz
- VideoNG
- OffscreenCanvas 相关能力
Architecture#
主要解释 Chromium 渲染管线、进程模型、线程模型和不同阶段之间的关系。
Chromium 渲染系统主要负责四件事:
- 把网页内容渲染成屏幕上的像素。
- 执行动画。
- 响应输入和滚动。
- 把输入事件路由到正确的 frame / element。
网页内容本身由很多部分组成:
- frame tree
- DOM
- CSS
- canvas
- images
- videos
- fonts
- SVG
渲染流水线阶段#
RenderingNG 把主渲染流水线拆成多个明确阶段:
Animate
→ Style
→ Layout
→ Pre-paint
→ Scroll
→ Paint
→ Commit
→ Layerize
→ Raster / Decode / Paint Worklet
→ Activate
→ Aggregate
→ Drawtext各阶段大致含义:
| 阶段 | 作用 |
|---|---|
| Animate | 更新动画状态 |
| Style | 根据 DOM 和 CSS 计算 Computed Style |
| Layout | 计算元素尺寸和位置,生成 Fragment Tree |
| Pre-paint | 生成 Property Trees,计算 Paint Invalidation |
| Scroll | 处理滚动相关状态 |
| Paint | 生成 Display List |
| Commit | 把 Main Thread 的渲染数据提交给 Compositor Thread |
| Layerize | 决定哪些内容进入 Composited Layers |
| Raster / Decode / Paint Worklet | 光栅化图块、解码图片、执行 Paint Worklet |
| Activate | 激活准备好的 Compositor Tree |
| Aggregate | Viz 聚合多个 CompositorFrame / Surface |
| Draw | 通过 GPU 输出最终像素 |
其中,某些动画和滚动不需要重新执行完整的 Style、Layout 和 Paint,可以直接在 Compositor Thread 上完成。这是 Chromium 能流畅处理滚动和部分 CSS 动画的重要原因。
进程模型#
RenderingNG 涉及几个重要进程:
-
Render Process
- 负责某个 site / tab 组合的页面渲染。
- 执行 DOM、CSS、Layout、Paint、Compositing 等工作。
- 跨站 iframe 可能运行在不同 Render Process 中。
-
Browser Process
- 负责浏览器 UI、标签页管理、权限、安全、输入事件分发等。
- 不直接执行网页 DOM / CSS / Layout / Paint。
-
Viz Process / GPU Process
- 负责接收多个 Render Process 和 Browser UI 提交的 CompositorFrame。
- 执行 Surface Aggregation。
- 最终通过 GPU API 上屏。
线程模型#
Render Process 内部主要有这些线程:
-
Main Thread
- 执行 JavaScript。
- 解析 HTML / CSS。
- 运行 Style、Layout、Pre-paint、Paint。
- 处理部分事件分发和 Hit Testing。
-
Compositor Thread
- 处理滚动。
- 处理部分动画。
- 管理 Commit 之后的 Compositor Lifecycle。
- 调度 Raster、Decode、Activate、Draw 等任务。
-
Raster Worker Threads
- 执行 Tile Raster 任务。
- 把绘制指令转换成 GPU / Skia 资源。
-
Media / Demuxer / Audio Output Threads
- 处理音视频解复用、解码、同步和输出。
Main Thread 和 Compositor Thread 的分离,是 Chromium 能够在 JavaScript 卡顿时仍然保持部分滚动和动画流畅的关键。
Key Data Structures#
这篇文章解释 RenderingNG 的几个核心数据结构。
主要包括:
Frame Trees
Immutable Fragment Tree
Property Trees
Display Lists / Paint Chunks
Compositor FramestextFrame Trees#
Chromium 中的 Frame Tree 包含两类 frame:
-
Local Frame
- 当前 Render Process 真正负责渲染的 frame。
- 拥有完整的 DOM、CSS、Layout 等数据。
-
Remote Frame
- 当前 Render Process 中的跨进程 frame 占位。
- 只保存尺寸、边界等最少信息。
- 真正内容由另一个 Render Process 渲染。
RenderingNG 的渲染流水线通常按 Local Frame Tree Fragment 粒度运行。最后,Viz 会收集多个进程提交的 CompositorFrame,再聚合成最终画面。
Immutable Fragment Tree#
Immutable Fragment Tree 是 Layout 阶段的输出。
它描述元素的:
- 位置
- 尺寸
- 行盒信息
- fragmentation 信息
一个 DOM element 通常对应一个 fragment,但在多栏布局、分页、打印等场景中,一个 element 可能对应多个 fragment。
Fragment 的重要特点:
- 生成后不可变。
- 子节点不能向上引用父节点。
- 可以复用旧 fragment。
- 方便做增量 layout。
这和旧版 mutable layout tree 不同。旧架构里 Layout Object 既保存输入,也保存输出,很容易导致状态污染和 invalidation bug。
Property Trees#
Property Trees 是 RenderingNG 中非常重要的概念。
它把 DOM / CSS 中复杂的视觉属性抽离成四棵树:
| Property Tree | 作用 |
|---|---|
| Transform Tree | 表达 transform 和坐标空间变化 |
| Clip Tree | 表达裁剪关系 |
| Effect Tree | 表达 opacity、filter、blend mode 等效果 |
| Scroll Tree | 表达滚动节点和滚动关系 |
每个 DOM element 会关联一个 Property Tree State:
(transform, clip, effect, scroll)text也就是一个四元组,用来描述这个元素受哪些 transform、clip、effect、scroll 影响。
Property Trees 的价值在于:
- 统一几何计算。
- 避免不同渲染子系统重复实现 transform / clip / scroll 逻辑。
- 让 Paint、Compositing、Hit Testing、Scrolling 等阶段共享同一套视觉属性语义。
Display Lists and Paint Chunks#
Paint 阶段会生成 Display List。
Display List 是按 CSS painting order 排列的一组绘制命令,例如:
DrawRect
DrawText
DrawImage
DrawPathtextPaint Chunk 是 Display List 中共享相同或相近 Property Tree State 的一段绘制内容。
可以粗略理解为:
PaintArtifact
├── DisplayItemList
│ ├── DisplayItem 0
│ ├── DisplayItem 1
│ ├── DisplayItem 2
│ └── ...
└── PaintChunk list
├── PaintChunk 0
├── PaintChunk 1
└── ...textLayerize 阶段会基于 Paint Chunks 做合成层划分。
如果全部内容放到一个 layer 中,滚动或局部变化可能导致大量内容重新 raster。
如果每个 Paint Chunk 都独立成 layer,又会消耗大量 GPU 内存和调度成本。
所以 layerization 本质是在:
减少重绘成本
vs
控制合成层数量和 GPU 内存text之间做权衡。
Compositor Frames#
CompositorFrame 描述如何把已经 raster 好的内容组合到屏幕上。
页面不会把整个 viewport 当成一个巨大 texture 提交,而是拆成多个 tiles。每个 tile 通常对应一个 GPU texture。
CompositorFrame 中包含 RenderPass,RenderPass 中包含 DrawQuads。
DrawQuad 描述:
- 使用哪个 texture
- 放在哪里
- 如何 transform
- 如何 clip
- 如何应用 visual effect
常见 DrawQuad 类型包括:
TextureDrawQuad
TileDrawQuad
SolidColorDrawQuad
RenderPassDrawQuad
YUVVideoDrawQuadtextSurface 则允许一个 CompositorFrame 嵌入另一个 CompositorFrame。
典型使用场景:
- Browser UI
- Site-isolated iframe
- video
- canvas
- plugin
Viz 的 Aggregate 阶段会把多个 Surface / CompositorFrame 聚合成最终显示结果。
BlinkNG#
BlinkNG 这篇文章讲的是 Blink 内部渲染架构的重构。
Blink 覆盖 compositing 之前的大部分渲染阶段:
Style
→ Layout
→ Pre-paint
→ Paint
→ Committext旧版 Blink 的问题是:虽然概念上也有这些阶段,但阶段边界不清晰。
旧架构中常见问题:
- 数据结构长期可变。
- 一个阶段的输出可能被另一个阶段随时修改。
- 很难判断某个数据是否已经是最终值。
- 很难判断某个阶段是否需要重新执行。
- Paint invalidation、compositing、layout 之间有循环依赖。
BlinkNG 的设计原则#
BlinkNG 希望让 Blink 渲染更像一个函数式 pipeline:
明确输入
→ 明确阶段
→ 明确输出text核心原则包括:
- 每个阶段有明确输入和输出。
- 阶段运行时输入稳定。
- 阶段输出在本轮 update 中不可变。
- 阶段结束时数据自洽。
- 同一件事尽量只计算一次。
- 用统一入口控制 Document Lifecycle。
DocumentLifecycle#
BlinkNG 使用 DocumentLifecycle 跟踪当前文档处于哪个渲染阶段。
例如:
Style clean
Layout clean
Pre-paint clean
Paint cleantext通过 lifecycle 状态和断言,可以避免在错误阶段访问或修改数据。
例如:
- 进入 Layout 前,Style 必须 clean。
- 进入 Paint 前,Pre-paint 必须 clean。
- Style clean 后,不应该再突然需要 style recalc。
Pre-paint 的正式化#
BlinkNG 把 Pre-paint 正式定义成独立阶段。
Pre-paint 主要负责:
- Paint invalidation。
- 生成 Paint Property Trees。
- 记录 pixel-snapped paint locations。
- 为 Paint 和 Compositing 准备稳定输入。
这使得 Paint 阶段可以更专注于生成绘制指令,而不是同时处理大量几何、失效和合成准备逻辑。
Composite After Paint#
Composite After Paint 是 BlinkNG 的关键变化。
旧架构中,layerization 在 Paint 之前进行。这会导致:
Compositing 依赖 Paint
Paint 又依赖 Compositing
Invalidation 也依赖二者text形成循环依赖。
Composite After Paint 的做法是:
Pre-paint
→ Paint
→ PaintArtifact
→ Layerization
→ Committext也就是先 Paint,再根据 PaintArtifact 和 Property Trees 做 layerization。
这样可以减少循环依赖,让合成层划分基于更稳定的 paint 输出。
VideoNG#
VideoNG 是和视频子系统最相关的一篇 RenderingNG 文章。
它解释了视频为什么不能简单地当成普通 DOM 内容来走完整的 Paint → Raster 路径。
视频播放的基本阶段#
视频播放可以拆成三部分:
Demuxing
→ Decoding
→ Renderingtext含义:
| 阶段 | 作用 |
|---|---|
| Demuxing | 把容器字节流拆成音频包和视频包 |
| Decoding | 把 encoded packets 解码成原始音视频帧 |
| Rendering | 决定什么时候显示这些已解码帧 |
例如,一个 MP4 文件里同时包含:
- 视频编码流
- 音频编码流
- metadata
- timestamps
Demuxer 负责拆出音频和视频 encoded packets。
Decoder 负责把 encoded packets 解码成:
- Audio frames
- Video frames
Renderer 负责根据时间戳和播放时钟,把帧在正确时间输出。
视频为什么特殊#
视频最大的问题是带宽。
例如 4K 60fps 视频,如果在 CPU / GPU 内存之间来回复制,很容易造成巨大的内存带宽压力。
因此 Chromium 尽量让视频数据:
decoder
→ GPU / platform buffer
→ compositor / overlay
→ displaytext在这个过程中减少 CPU 拷贝。
安全隔离#
媒体解析和解码是安全风险较高的区域。
Chromium 采用 defense-in-depth:
- Demuxing 和软件解码通常运行在低权限进程中。
- 硬件解码运行在具备最小 GPU 访问权限的进程中。
- 跨进程通信依赖 Mojo。
- 不同平台硬解码能力通过统一抽象接入。
视频与普通渲染流水线的关系#
VideoNG 里的一个关键观点是:
从主渲染流水线看,video 更像是一个固定尺寸、带透明度的洞。
也就是说:
DOM / CSS
→ 决定 video 元素的位置、尺寸、clip、transform、opacity
→ media pipeline 解码出 VideoFrame
→ VideoFrame 通过 Surface / Viz / Overlay 接入最终合成text视频内容本身通常不完全走普通 DOM 的 Paint → Raster 路径。
普通 DOM 内容:
DOM
→ Style
→ Layout
→ Paint
→ Raster
→ DrawQuad
→ CompositorFrametext视频内容更接近:
HTMLVideoElement
→ WebMediaPlayerImpl
→ media pipeline
→ Demuxer
→ Decoder
→ VideoFrame
→ Surface / Overlay / Viz
→ Displaytext这也是后续阅读视频子系统时最重要的连接点。
Surface 与 Overlay#
VideoNG 使用 Surface 让 video 可以直接和 Viz 交互。
不同平台有不同的硬件合成机制,例如:
| 平台 | 机制 |
|---|---|
| Windows | Direct Composition / Media Foundation |
| macOS | CoreAnimation Layers / VideoToolbox |
| Android | SurfaceView / MediaCodec |
| Linux | VA-API / VASurfaces |
Chromium 通过统一抽象处理这些平台差异,例如:
OverlayProcessor
mojo::VideoDecoder
VideoFrame
SurfacetextPull-based Playback Pipeline#
Chromium 的媒体播放栈采用 pull-based 架构。
可以粗略理解为:
Renderer 请求帧
→ Decoder 请求 encoded packet
→ Demuxer 请求数据
→ DataSource 请求字节
→ Network / File IOtext音频通常推进播放时钟,Video Renderer 根据 presentation timestamp 决定显示哪一帧。
如果视频帧率低于屏幕刷新率,同一帧可能重复显示。
如果视频帧率高于屏幕刷新率,部分帧可能被跳过。
VideoNG 的核心结论#
VideoNG 的重点不是讲 <video> API,而是讲:
视频帧如何绕开普通 Paint / Raster 路径,
通过 Surface / Overlay / Viz 高效进入最终合成。text这和普通 RenderingNG pipeline 是互补关系。
LayoutNG#
LayoutNG 是 Blink layout 系统的重写。
旧版 layout 系统的问题主要来自 mutable layout tree。
旧架构中,一个 Layout Object 同时保存:
- 父节点给它的约束。
- 自己的 style。
- layout 输入状态。
- layout 输出结果。
- 上一次 layout 遗留的数据。
这会导致很多问题:
- correctness bug
- under-invalidation
- over-invalidation
- hysteresis
- 非线性性能退化
LayoutNG 的基本模型#
LayoutNG 把 layout 建模成:
输入:
DOM + Computed Style + Parent Constraints
输出:
Immutable Fragment TreetextLayout Tree 仍然存在,但它更像是 layout 输入和关联关系的结构。
真正的 layout 输出是 immutable fragments。
Parent Constraints#
LayoutNG 会把产生 fragment 的 parent constraints 存下来,作为缓存 key。
这样下一次 layout 时,可以比较:
旧 constraints
vs
新 constraintstext如果 constraints 没变,就可以复用旧 fragment。
如果 constraints 变了,才需要重新 layout。
Under-invalidation#
Under-invalidation 指的是:
某个 Layout Object 被错误认为 clean,
但实际上它受到父约束变化影响,输出已经不正确。text旧 mutable tree 很容易出现这种问题。
LayoutNG 通过保存 immutable parent constraints 和 immutable fragment,可以在集中位置判断 child 是否真的需要重新 layout。
Hysteresis#
Hysteresis 指的是:
相同输入重复 layout,却得到不同输出。text这通常是因为 layout 过程中错误读取了上一次 layout 留下的 mutable state。
LayoutNG 通过明确输入和输出,并减少对旧状态的依赖,降低这类问题。
Two-pass Layout#
flex / grid 等布局经常需要 two-pass layout:
- 第一轮测量 children。
- 第二轮根据测量结果做最终布局。
如果没有良好的缓存和 constraints 管理,复杂嵌套可能导致 layout 在 measure state 和 final state 之间反复震荡,产生严重性能问题。
LayoutNG 通过 fragment cache 和 constraints cache,让这类布局更可控。
LayoutNG 的核心结论#
LayoutNG 的核心是:
不要把 layout 输出长期保存在 mutable tree 上,
而是把 layout 变成:
稳定输入 → 不可变输出。text这和 BlinkNG 的整体思路一致。
LayoutNG Block Fragmentation#
Block fragmentation 主要处理多栏、分页、打印等场景。
Fragmentainer 指的是可以容纳 fragment 的区域,例如:
- 多栏布局中的一列
- 打印中的一页
- 分页媒体中的一页
它不是 DOM 元素,而是布局过程中的容器概念。
旧 fragmentation 引擎的问题#
旧引擎并不是真正在 layout 阶段切分内容。
它更像是:
先把内容排成一个很高的虚拟长条
→ 再在 pre-paint / paint 阶段用 clip 和 translate 把内容切到不同列 / 页text这种方式会导致很多问题:
- relative positioning 表现错误
- transform 表现错误
- box-shadow / text-shadow 跨列表现错误
- flex / grid fragmentation 支持不足
- hit testing 和 painting 难以正确处理
LayoutNG Fragmentation 的做法#
LayoutNG 在 layout 阶段真正处理 fragmentation。
大致流程:
在当前 fragmentainer 中 layout
→ 遇到 break
→ 生成当前 fragmentainer 的 fragments
→ 保存 break token
→ 进入下一个 fragmentainer
→ 从 break token 恢复 layouttext关键数据结构:
| 数据结构 | 作用 |
|---|---|
NGBlockBreakToken | 保存下一列 / 下一页恢复 layout 所需的信息 |
NGEarlyBreak | 记录更好的提前断点,例如满足 break-before: avoid、orphans、widows 等规则 |
核心结论#
LayoutNG fragmentation 的关键变化是:
从 paint 阶段假装切块,
变成 layout 阶段真正生成多个 fragments。text这让复杂分页、多栏和打印场景更正确。
CVD Color Vision Deficiency Simulation#
这篇文章和 RenderingNG 主流水线关系不如前几篇强,但它展示了 Blink 内部如何复用 Web Platform 能力实现 DevTools 功能。
CVD 指 color vision deficiency,也就是色觉缺陷。
DevTools 中的色觉缺陷模拟功能可以帮助开发者理解页面在不同视觉条件下的显示效果,例如:
- deuteranopia
- protanopia
- tritanopia
- achromatopsia
实现思路#
色觉缺陷模拟本质上是对最终像素做颜色矩阵变换。
可以用 Web 技术表示为:
CSS filter
→ SVG feColorMatrix
→ 对每个像素的 RGBA 值做矩阵变换text为什么不能直接修改页面 DOM#
一种简单做法是往页面里注入 SVG 和 CSS filter。
但这会带来问题:
- 污染页面 DOM。
- 可能和页面已有元素 ID 冲突。
- 可能覆盖页面已有 filter。
- 会被开发者在 DevTools 里看到。
- 可能受到页面 CSP 限制。
Blink 内部实现#
最终实现使用 data URL 内联 SVG filter,并把 filter 应用到 Blink 内部 viewport 对象上,而不是页面 DOM 上。
这样可以做到:
视觉上影响页面
但不污染页面 DOM / CSSOMtext核心结论#
这篇文章的价值在于说明:
Blink 内部功能也可以复用 Web Platform 自己的抽象,
而不是重新实现一套完全独立的颜色处理系统。textRenderingNG 的整体主线#
RenderingNG 可以理解为 Chromium 渲染系统从旧架构到新架构的系统性重构。
旧架构的问题:
长生命周期 mutable objects
阶段边界模糊
Paint / Compositing / Invalidation 循环依赖
Main Thread 压力大
跨进程 iframe、视频、滚动、动画难以统一优化textRenderingNG 的方向:
明确 pipeline stage
明确每个阶段的输入和输出
使用不可变中间产物
用 Property Trees 统一几何 / 裁剪 / 效果 / 滚动语义
Paint 之后再做 Compositing
用 Viz 统一聚合和上屏
用 LayoutNG 重写 layout
用 VideoNG 让视频通过 Surface / Overlay / Viz 高效接入合成text对源码阅读的指导意义#
阅读 Chromium 渲染源码时,可以按这条线建立地图:
DOM / CSS / JS
→ Style
→ Layout
→ Pre-paint
→ PaintArtifact
→ Layerization
→ Commit
→ Raster
→ DrawQuad
→ CompositorFrame
→ Viz Aggregation
→ Displaytext对应到视频子系统时,则应该把 VideoNG 叠加进来:
HTMLVideoElement
→ HTMLMediaElement
→ WebMediaPlayerImpl
→ media pipeline
→ Demuxer
→ Decoder
→ VideoFrame
→ Surface / Overlay
→ Viz
→ Displaytext普通页面内容主要解释为:
RenderingNG:DOM / CSS 如何变成 CompositorFrametext视频内容主要解释为:
VideoNG:VideoFrame 如何绕开普通 Paint / Raster 路径,
通过 Surface / Overlay / Viz 进入最终合成。text