Chromium 源码学习笔记(一):一个 URL 输入之后,浏览器里发生了什么?

系列第一篇,目的是建立第一张 Chromium 架构地图。 写给想开始读浏览器源码、但不知道从哪里下手的人,也写给未来复习的自己。


为什么先画一张地图?

最近在系统学 Chromium 源码。

Chromium 是 Chrome 浏览器的开源内核,也是大部分基于 Blink 的浏览器的基础。它的源码非常大,光 src/ 目录下就有几百个子目录,涉及的 C++ 类动辄几千个。

一开始我就犯了一个错误:从某个函数开始追,越追越深,最后忘了自己最开始要解决什么问题。

后来我意识到,读大型工程需要先有一张地图。

这张地图的核心问题只有一个:

用户输入一个 URL 后,Chromium 里大概有哪些模块参与,它们分别在做什么?

这篇文章就是我的第一张地图。不追求全面,只追求能复述。


Chromium 是一套协作系统,不是一个大程序

先给一个整体印象。

我目前把 Chromium 大致分成这几层:

层次 职责 关键目录
产品层 UI、Tab、地址栏、平台接入 chrome/android_webview/
content/ 页面、导航、frame、renderer process 调度 content/
Blink + V8 HTML/CSS/DOM 解析,JavaScript 执行 third_party/blink/v8/
Network 资源加载 services/network/net/
cc / Viz / GPU 合成、聚合、屏幕显示 cc/components/viz/gpu/
Mojo 跨进程 IPC 通信基础设施 mojo/
base/ 任务调度、回调、WeakPtr、日志等 base/

但这些模块不是简单的上下级,而是围绕"页面加载和显示"这条运行时链路协作的。


多进程:理解 Chromium 的第一道门

看 Chromium 源码之前,最值得先弄清楚一件事:

Chromium 为什么要多进程?

答案不是"为了让架构更优雅",而是现实需要。

网页内容不可信。渲染引擎可能崩溃。如果所有网页都跑在同一个进程里:

text 复制代码
一个页面崩溃
  -> 渲染引擎崩
  -> 整个浏览器崩
  -> 所有 tab 一起没了

所以 Chromium 把浏览器拆开:

text 复制代码
Browser Process       <- 浏览器的"可信控制中心"
  UI、tab、导航、权限、进程管理

        ↕ Mojo / IPC

Renderer Process      <- 网页内容的"隔离执行环境"
  Blink、V8、DOM、layout、paint

        ↕ Mojo / IPC

Network / GPU / Service Process
  资源加载、图形输出、设备能力

这里有一个命名规律很有用:

text 复制代码
带 Host 的对象,一般是 Browser 侧代表 renderer 里某个东西。

比如 RenderProcessHost 代表一个 renderer process,RenderFrameHost 代表 renderer 里某个 frame。Browser 侧通过这些 Host 对象和 Mojo / IPC 跟 renderer 通信,而不是直接拿对象指针。

明白了多进程模型,后面很多 Chromium 的设计就顺了。


URL 到首帧:八步

接下来是主线。以用户在地址栏输入 https://example.com 为例,Chromium 内部大概会经历这些阶段:

text 复制代码
用户输入 URL
  ↓
[1] 产品层:整理打开意图
  ↓
[2] content:创建 NavigationRequest,组织导航
  ↓
[3] Network:加载主资源 HTML
  ↓
[4] CommitNavigation:Browser → Renderer 交接
  ↓
[5] Blink + V8:解析 HTML,执行 JS,加载子资源
  ↓
[6] Blink:style → layout → paint
  ↓
[7] cc:合成,生成 CompositorFrame
  ↓
[8] Viz / GPU:present 到屏幕

逐步看一下每一段。


1 产品层:表达"我要打开这个 URL"

用户输入 URL 时,最先接手的是产品层,不是 Blink 或 Network。

text 复制代码
PC Chrome        → chrome/
Android WebView  → android_webview/

产品层负责地址栏、Tab、菜单、权限弹窗这些用户能直接看到的东西。它不解析 HTML,也不执行 JavaScript。

它的核心职责只是把用户操作翻译成一个意图交给 content

text 复制代码
请在这个 WebContents 里打开这个 URL。

地址栏的输入可能是 URL、搜索词,也可能是历史记录或自动补全结果,产品层需要先判断最终打开什么。


2 content:一次导航的调度中心

产品层把意图交给 content 后,真正的导航才开始。

我目前对 content 的一句话理解:

content 把产品层的意图,变成浏览器内核里的页面运行流程。

它要负责这次导航的全局调度:

  • 哪个 WebContents 承载这个页面。
  • 这是新导航、刷新,还是历史跳转。
  • 是否需要新的 renderer process。
  • 旧页面如何退出,新文档如何接管。

NavigationRequest 是这次导航的"任务单",记录 URL、目标 frame、发起来源、redirect 状态、响应信息、commit 状态等。

第一轮可以先记几个源码入口:

text 复制代码
NavigationControllerImpl::LoadURLWithParams
NavigationRequest::Create
NavigationRequest::BeginNavigation

这里最重要的一个认知是:

Navigation 不是单纯的网络请求,而是一次页面跳转从发起到 commit 的生命周期管理。

它不只要"下载 HTML",还要处理安全策略检查、redirect、下载分流、错误页、目标 frame、renderer process 选择等等问题。


3 Network:拿回主资源

导航进入加载阶段后,需要拿到主资源,也就是主 HTML 文档。

主资源加载的大致路径:

text 复制代码
NavigationRequest
  → NavigationURLLoader
  → ResourceRequest
  → URLLoaderFactory → URLLoader
  → Network Service
  → net(DNS、TCP、TLS、HTTP、缓存、代理...)
  → response headers + body stream

注意这里 Network 返回的不是一个字符串,而是两部分:

  • response headers:让上层先判断响应类型,比如状态码、MIME type、是否 redirect、是否触发下载。
  • body stream:后续 Blink 才真正消费的 HTML 字节流。

services/network 负责资源加载服务的入口和策略,net 负责更底层的网络能力。

子资源(CSS、JS、图片等)不是在这里加载的,而是 Blink 解析 HTML 时自己去发起,最终也走同一套 Network Pipeline。


4 CommitNavigation:Browser 和 Renderer 的交接点

拿到主资源响应之后,并不是直接把 HTML 交给 Blink 就行了。

content 还需要决定:

text 复制代码
提交到哪个 frame?
使用哪个 RenderFrameHost?
是否需要换 renderer process?
旧页面如何退出?

CommitNavigation 是我第一轮重点记的分界线:

text 复制代码
commit 前:Browser Process / content 主导导航、检查、加载、进程和 frame 选择。
commit 后:Renderer Process / Blink 开始创建 Document、解析 HTML、加载子资源和渲染。

网络响应回来 ≠ 新页面已经生效。只有 content 通过 Mojo IPC 把 commit 参数和 body stream 发给 renderer,Blink 才真正开始处理新 Document。


Renderer 收到 commit 后,Blink 创建 Document / DocumentLoader,开始消费 HTML body stream:

text 复制代码
HTML bytes → HTML parser → tokens → DOM nodes → DOM tree

解析过程中,Blink 会发现 CSS、JS、图片等子资源,再次触发 Network Pipeline 去加载。

Blink 和 V8 的分工:

text 复制代码
Blink:Web 文档结构、样式、布局、绘制语义、Web API。
V8:JavaScript 运行时,脚本编译和执行。

JavaScript 不是孤立地在 V8 里运行。网页里的 JS 通常通过 Blink 暴露的 DOM / Web API 影响页面。比如:

js 复制代码
document.querySelector("h1").innerText = "Hello Chromium";

执行过程可以这样理解:

text 复制代码
V8 执行 JS
  → Blink 提供 DOM / Web API
  → JS 修改 DOM
  → Blink 更新页面状态
  → 触发后续 style / layout / paint

6 Blink:style → layout → paint

当 DOM 和 CSS 规则都准备好了(可能已经被 JS 修改过),Blink 进入渲染计算:

style calculation:算出每个元素最终应用什么样式(computed style)。

layout:算出每个元素放在哪里、占多大(layout tree + geometry)。

paint:决定每个东西怎么画(背景、边框、文字、图片...),生成绘制指令列表。

这里要记一个点:

paint records 还不是像素,只是"应该怎么画"的记录。

真正产生像素要到合成和显示阶段。


7 cc:合成,生成 CompositorFrame

绘制记录接下来进入合成体系(cc):

text 复制代码
paint records
  → layerization(哪些内容独立成层)
  → cc layer tree
  → raster / tiles(栅格化为纹理)
  → DrawQuads
  → CompositorFrame

几个值得注意的点:

  • DOM tree ≠ layer tree。Chromium 会按需把内容提升为独立层,比如有动画的元素、video、canvas、position:fixed、滚动区域。
  • 页面不会一次性光栅化成一张大图。Chromium 分 tile 并行处理,视口外的内容可以懒处理。
  • CompositorFrame 是 renderer 提交给 Viz 的帧数据,但还不是最终屏幕画面。

8 Viz / GPU:present 到屏幕

最终屏幕画面可能来自多个来源:主页面、跨进程 iframe、视频 surface、浏览器 UI......

Viz 负责统一聚合这些 CompositorFrame

text 复制代码
CompositorFrame(来自多个 renderer)
  → Surface(每个来源对应一个 Surface)
  → SurfaceAggregator(聚合为最终帧)
  → AggregatedFrame
  → SkiaRenderer / GPU
  → SwapBuffers / present
  → 屏幕

到这一步,用户才真正看到页面首帧。


用数据形态串一遍

比起记模块名,我更喜欢追"数据在链路里的形态变化",这样不容易混乱:

text 复制代码
用户输入文本
  → 最终 URL
  → LoadURLParams
  → NavigationRequest
  → ResourceRequest
  → response headers + body stream
  → commit params + body stream
  → Document / DocumentLoader
  → HTML tokens → DOM tree
  → CSS rules → computed style
  → V8 执行 JS / DOM mutation
  → layout geometry
  → paint records / PaintArtifact
  → composited layers → tiles / raster resources
  → DrawQuads → CompositorFrame
  → Surface → AggregatedFrame
  → GPU buffer → screen pixels

这条数据变化链路贯穿了产品层、content、Network、Blink、V8、cc、Viz、GPU。


我总结的 Chromium 源码阅读方法

写到这里,我觉得第一篇最重要的收获不是记住了哪些类,而是建立了一种读 Chromium 的方法。

以前我会问:

text 复制代码
这个类是干什么的?
这个函数在哪里被调用?
这段代码具体怎么跑?

这些问题当然要问,但如果一开始就钻进去,很容易忘了自己在哪个链路里。

现在我会先问五个问题:

问题 作用
它接收什么输入? 定位它在链路中的起点
它产出什么? 定位它的终点和交接对象
它在哪个进程/线程? 定位它的执行环境
它把事情交给谁? 理解 handoff,而不只是 helper
出问题怎么反查? 从症状反推到模块,而不是从代码猜症状

这套方法的核心是:

先理解责任边界,再理解内部实现。 先能讲清主线,再深入状态机和分支。

对于 Chromium 这类大型工程,这比从头开始追调用栈更适合第一轮入门。


这篇先不展开什么

说清楚边界很重要。以下内容我目前都只是"知道有这个东西",还没有深入:

  • BFCache / Prerender / FencedFrame
  • SiteInstance / RenderFrameHost 生命周期状态机
  • Network 的 Cookie、Cache、CORS、HTTP/2、QUIC
  • V8 Ignition / TurboFan / GC
  • cc scheduler、raster priority
  • Viz surface lifetime、overlay 细节
  • FP / FCP / LCP 等性能指标的精确定义

第一轮目标只是:

能从 URL 输入讲到首帧显示,知道每个模块在链路里大概负责什么。


最后:一段话总结

用户输入 URL 后,产品层把意图传给 contentcontent 创建 NavigationRequest,完成导航调度和主资源加载;Network Service / net 取回 HTML 响应;content 通过 CommitNavigation 把响应交给 renderer;Blink 创建 Document、解析 HTML、加载 CSS / JS / 图片;V8 执行 JavaScript 并可能修改 DOM;Blink 计算 style / layout / paint;cc 合成并生成 CompositorFrame;Viz 聚合多个 Surface;GPU 最终 present 到屏幕。

Chromium 打开一个页面,不是简单的"下载 HTML 然后显示",而是一条跨多个进程、多个模块的运行时协作链路。


下一篇预告

下一篇我打算把这条链路里最值得深入的一段单独拆开看:

Navigation Pipeline:一次 URL 如何变成新页面?

重点会看:

  • NavigationRequest 到底记录了什么。
  • Navigation 为什么不只是网络请求。
  • NavigationURLLoader 如何进入资源加载栈。
  • CommitNavigation 为什么是 Browser 和 Renderer 的关键分界线。

这是一篇学习笔记,不是官方文档。第一轮建立主线,后续持续修正。如果有理解偏差欢迎指出 : ) 写给想开始读浏览器源码、但不知道从哪里下手的人,也写给未来复习的自己。

相关推荐
头茬韭菜2 小时前
4.1-4.2 工具注册 — Fail-Closed 类型安全设计与四层工具架构
java·安全·架构
索西引擎2 小时前
【React】React Fiber 架构:设计动机、核心机制与调度策略分析
前端·react.js·架构
Sirius Wu2 小时前
OpenClaw SubAgent 三层安全约束完整详解
人工智能·安全·ai·架构·aigc
mCell9 小时前
从 VS Code 学系统架构:回到当时,理解它为什么这样演进
架构·源码阅读·visual studio code
国科安芯13 小时前
航天电子模拟前端三大支柱:精密运放、高速运放与电压监控的协同设计方法——ASL8522S/ASL622S/ASL706S技术解析
前端·单片机·嵌入式硬件·fpga开发·架构·安全性测试
listening77713 小时前
HarmonyOS 6.1 全场景开发终篇:从单设备到万物互联的实战复盘与架构思考
华为·架构·harmonyos
heimeiyingwang15 小时前
【架构实战】缓存一致性:Cache Aside与双写问题的破解
spring·缓存·架构
feiyu_gao17 小时前
别再让 AI 从零开始了
人工智能·架构·aigc
通问AI17 小时前
GPT-5.6 三模型架构深度解析:Sol/Terra/Luna 技术对比与工程落地指南
gpt·架构