一、从URL到像素的完整旅程
一次页面加载涉及网络、解析、渲染三大阶段,下图画出完整链路与每一步对应的 Chromium 源码入口。
图 1-1:从 URL 到像素的完整管线 — 每一步标注了 Chromium 源码入口与典型耗时
🏠 生活类比:盖房子全流程
URL = 地址 — 你知道要去哪
DNS = 114查号台 — 先查本地通讯录,没有再一级级往上问
TCP = 打电话建立连接 — "喂?能听到吗?" "能!" "那我说了"
TLS = 加密通话 — 先交换密钥,再说正事防窃听
HTML = 建筑蓝图 — 哪里是墙、哪里是门
CSSOM = 装修方案 — 墙什么色、门多宽
Render Tree = 去掉隐藏房间的蓝图 — display:none 的不画
Layout = 量尺寸 — 算出每面墙的确切位置和大小
Paint = 刷漆 — 按尺寸给每面墙上色
Composite = 拼图层 — 把多层透明画纸叠在一起贴到屏幕
// chromium/renderer/render_pipeline.cc — 渲染主管线伪代码
void RenderPipeline::Execute(Document* doc) {
// 阶段1: 构建 DOM + CSSOM
while (!tokenizer_.IsEOF()) {
Token token = HTMLParser::ProcessToken(); // ← 逐Token构建DOM
if (token.IsScript()) {
HTMLParser::PauseParsing(); // ← 遇script阻塞
V8::ExecuteScript(token.Source());
HTMLParser::ResumeParsing();
}
}
CSSOM = CSSParser::ParseStyleSheet(css_tokens); // ← 构建CSSOM
// 阶段2: 构建 Render Tree (DOM + CSSOM → Render Tree)
RenderTree = ComputedStyle::Create(DOM, CSSOM);
// 阶段3: 渲染管线 Layout → Paint → Composite
LayoutObject::Layout(RenderTree); // ← 计算几何
PaintController::DrawRecord(LayoutTree); // ← 生成绘制记录
CompositingManager::Update(PaintRecords); // ← GPU合成
}
二、DNS 解析 + TCP 连接 + TLS 握手
在拿到任何数据之前,浏览器需要先找到服务器、建立连接、加密通道。
图 2-1:DNS 递归解析 — 从浏览器缓存逐级到权威 DNS,← net::HostResolver::Resolve()
图 2-2:TCP 三次握手 — SYN → SYN+ACK → ACK,← TCPConnect() 三次握手入口
图 2-3:TLS 1.3 握手 — 仅需 1-RTT,← SSLClientSocket::Connect() TLS握手入口
📞 生活类比
DNS = 114查号台 — 先查本地通讯录(浏览器缓存),没有再一级级问(路由器→ISP→根→TLD→权威)
TCP = 打电话 — SYN="我能打给你吗?",SYN+ACK="可以!",ACK="那我开始了"
TLS = 加密通话 — 先交换密钥确认身份,再说正事防窃听。TLS 1.3 只需 1 次来回
// net/dns/host_resolver.cc — DNS解析入口
int HostResolver::Resolve(const HostPortPair& host) {
// 1. 查浏览器缓存
if (HostCache::Lookup(host, &addr)) return addr;
// 2. 查操作系统缓存
if (getaddrinfo(host.hostname, ...)) return addr;
// 3. 发起递归查询
return DNSTask::Start(host); // → Root → TLD → Auth
}
// net/socket/ssl_client_socket.cc — TLS握手
int SSLClientSocket::Connect() {
SendClientHello(); // ← ClientHello + Key Share
RecvServerHello(); // ← ServerHello + Cert
VerifyCertificate(); // ← 证书链验证
DeriveKeys(); // ← 密钥派生
return OK; // ← 加密通道建立
}
三、HTML/CSS 解析与 DOM 构建
HTML 字节流如何一步步变成 DOM Tree?CSS 如何变成 CSSOM?两者如何合成 Render Tree?
图 3-1:HTML 解析管线 — 字节流经编码、分词、建树三步变成 DOM Tree
图 3-2:DOM + CSSOM 合成 Render Tree — display:none 的节点不进入 Render Tree,<script> 阻塞解析
🏗️ 生活类比:盖房子
DOM = 房子的结构图 — 哪里是墙、哪里是门、哪里是窗(元素层级关系)
CSSOM = 装修方案 — 墙什么颜色、门多宽、窗用玻璃还是木框(样式规则)
Render Tree = 去掉不需要装修的房间 — display:none 的房间不出现在施工图上
Script = 施工队 — 来了就停工等指示!async=施工队自己先看图纸,defer=等设计完再施工
性能提示
CSS 放 <head>(尽早构建 CSSOM),JS 放 </body> 前或加 defer,避免阻塞 DOM 解析。
// third_party/blink/core/html/html_document_parser.cc
void HTMLDocumentParser::ProcessToken(Token& token) {
switch (token.GetType()) {
case kStartTag:
InsertElement(token.GetName()); // ← 创建 DOM 节点
if (token.IsScript()) {
PauseParsing(); // ← 阻塞!等 JS 执行
ScriptLoader::FetchAndExecute(token);
}
break;
case kEndTag:
CloseElement(token.GetName());
break;
case kCharacter:
InsertText(token.Data());
break;
}
}
// third_party/blink/core/css/css_parser.cc
void CSSParser::ParseRule(const String& css_text) {
StyleRule* rule = ConsumeRule(tokens); // ← 解析CSS选择器+声明
CSSOM::AddRule(rule); // ← 加入CSSOM树
}
// third_party/blink/core/style/computed_style.cc
void ComputedStyle::Create(Node* node, CSSOM* cssom) {
auto* style = MatchRules(node, cssom); // ← 样式匹配
if (style->display == EDisplay::kNone)
return; // ← 不生成 RenderObject
RenderTree::Attach(node, style);
}
四、渲染管线 — Layout → Paint → Composite
Render Tree 生成后,浏览器依次经过 Layout(布局)、Paint(绘制)、Composite(合成)三大步骤把页面渲染到屏幕。
图 4-1:Layout 阶段 — CSS 属性计算出像素位置,← LayoutObject::Layout()
图 4-2:Paint 阶段 — 按特定顺序生成绘制指令,再光栅化为像素位图
图 4-3:Composite 阶段 — 多图层由 GPU 合成,← CompositingManager::Update()
图 4-4:Reflow / Repaint / Composite-Only — 不同 CSS 属性变更触发不同级别的渲染更新,← StyleChangeType 判断变更级别
🎨 生活类比:画布作画
Layout = 画草图量尺寸 — 先用铅笔画出每个元素的位置和大小
Paint = 按草图上色 — 按背景→内容→轮廓的顺序涂颜色
Composite = 把多个透明图层叠在一起 — 每层单独画好,最后叠在一起看
Reflow = 改结构要重画 — 墙要拆了重建,草图、上色全重来
Repaint = 只换颜色不改建 — 草图不变,只重新上色
Composite-Only = 只移动便利贴不动底层 — 便利贴换个位置,底层画好的完全不动
Pro
动画优先使用 transform 和 opacity,只触发 Composite-Only 路径,可达 60fps。避免在动画中使用 width/height/top/left,这些会触发 Reflow。
// blink/renderer/core/layout/layout_object.cc
void LayoutObject::Layout() {
// 1. 计算自身尺寸 (根据 CSS Box Model)
ComputeSelfSize(); // ← width/height/padding/margin/border
// 2. 遍历子元素,分配空间
for (LayoutObject* child : Children()) {
child->Layout(); // ← 递归布局子元素
}
}
// blink/renderer/core/paint/paint_controller.cc
void PaintController::DrawRecord(LayoutObject* obj) {
PaintBackground(obj); // ← 1. 背景
PaintFloats(obj); // ← 2. 浮动
PaintForeground(obj); // ← 3. 前景内容
PaintOutline(obj); // ← 4. 轮廓
RasterBuffer::Rasterize(records); // ← 光栅化
}
// blink/renderer/platform/graphics/compositing_manager.cc
void CompositingManager::Update() {
AssignLayers(); // ← 决定哪些元素独立成层
RasterLayers(); // ← GPU 光栅化各层
CompositeLayers(); // ← GPU 合成 → 屏幕
}
// StyleChangeType 判断变更级别
auto change = ComputeStyleChange(old_style, new_style);
if (change >= kLayout) Reflow(); // ← 全管线
elif (change >= kPaint) Repaint(); // ← 跳过Layout
elif (change >= kComposite) Recomposite(); // ← 仅合成
五、JS 执行与事件循环
V8 引擎如何编译执行 JS?事件循环如何调度同步/异步任务?理解这一点才能写出高性能的前端代码。
图 5-1:V8 编译管线 — Source → AST → Ignition字节码 → (热点) → TurboFan优化 → 机器码,← v8::ScriptCompiler::Compile()
图 5-2:Event Loop 循环 — 同步→微任务→rAF→渲染→宏任务,← Chromium MessageLoop
async/await 如何映射到微任务
async/await 是 Promise 的语法糖。每个 await 之后的代码,等价于放入 .then() 的微任务回调。
| 写法 | 等价形式 | 执行类型 |
| await promise | promise.then(后续代码) | 微任务 |
| async function | 返回 Promise | 同步入口+微任务续延 |
| Promise.then() | 直接注册微任务 | 微任务 |
| setTimeout() | 定时器到期后入宏任务队列 | 宏任务 |
🏢 生活类比:办公室工作
Call Stack = 办公桌 — 正在处理的文件,一次只能做一件
Microtask = 领导紧急批示 — 必须马上办,办完手头的文件前先处理
Macrotask = 收件箱 — 办完所有紧急批示后,从收件箱取一件
rAF = 办完文件先去给领导汇报 — 渲染前汇报,汇报完再回来看收件箱
V8 = 翻译官 — 把 JS 翻译成 CPU 能懂的指令,热点的部分翻译成最高效的版本
// chromium/base/message_loop.cc — 事件循环核心伪代码
void MessageLoop::Run() {
while (running_) {
// 1. 取一个宏任务
Task task = DeferOrRunPendingTask(); // ← 从Macrotask队列取
if (task) task.Run();
// 2. 清空微任务队列
while (!microtask_queue_.IsEmpty()) {
Microtask mt = microtask_queue_.Take();
mt.Run(); // ← Promise.then / MutationObserver
}
// 3. requestAnimationFrame 回调
RunAnimationFrameCallbacks(); // ← rAF
// 4. 渲染(如果需要)
if (needs_render_) {
RenderPipeline::Execute(); // ← Layout→Paint→Composite
}
}
}
// v8/src/microtask-queue.cc
void MicrotaskQueue::RunMicrotasks() {
while (!queue_.IsEmpty()) {
Microtask mt = queue_.Dequeue();
mt->Run(); // ← 执行每个 Promise.then 回调
}
}
六、面试速答
浏览器渲染核心问题,一句话命中要害。
Q1:从输入URL到页面展示发生了什么?
答:URL → DNS解析(IP) → TCP三次握手 → TLS握手(HTTPS) → HTTP请求 → 服务器返回HTML → HTML解析构建DOM → CSS解析构建CSSOM → DOM+CSSOM合成Render Tree → Layout计算几何 → Paint生成绘制指令 → Composite GPU合成 → 像素到屏幕。关键源码入口:NavigationHandle::CommitNavigation()
Q2:什么是回流(Reflow)和重绘(Repaint)?如何避免?
答:Reflow 是几何属性变化(width/height/margin/padding)导致整个渲染管线重跑(Layout+Paint+Composite)。Repaint 是视觉属性变化(color/background)跳过Layout只重绘+合成。避免方法:① 批量修改DOM(DocumentFragment)② 避免逐条改style,用class切换 ③ 动画用 transform/opacity 只触发 Composite-Only ④ will-change 提升合成层。← StyleChangeType 判断变更级别
Q3:为什么 <script> 会阻塞解析?async 和 defer 有什么区别?
答:JS 可能修改 DOM(如 document.write),所以遇到 <script> 必须 pause parsing 等执行完。区别:① async:并行下载,下载完立即阻塞执行(顺序不确定)② defer:并行下载,等 DOM 解析完、DOMContentLoaded 前按顺序执行 ③ 无属性:完全阻塞下载+执行。← HTMLParser::PauseParsing()
Q4:事件循环中微任务和宏任务的执行顺序?
答:每次 Event Loop:① 执行一个宏任务 ② 清空所有微任务(Promise.then/MutationObserver)③ 执行 rAF 回调 ④ 渲染 ⑤ 取下一个宏任务。所以 Promise 先于 setTimeout。← MicrotaskQueue::RunMicrotasks() 在每个宏任务后清空
Q5:CSS 如何阻塞渲染?为什么 CSS 放头部?
答:CSS 不阻塞 DOM 解析,但阻塞 Render Tree 构建(没有 CSSOM 无法计算样式),也就阻塞渲染。CSS 放头部是因为:① 尽早开始 CSSOM 构建 ② 避免闪烁(FOUC)③ CSSOM 构建期间 JS 执行也会被阻塞(JS 可能查询样式)。← CSSParser::ParseRule() 必须完成后才能 ComputedStyle::Create()
Q6:V8 的 JIT 编译为什么快?什么情况下会 Deopt?
答:V8 先用 Ignition 解释执行字节码,收集类型反馈;热点代码经 TurboFan 优化编译为机器码(内联+类型特化),接近 C++ 性能。Deopt 触发条件:① 函数参数类型变了(多态)② 优化假设被推翻 ③ 使用了 eval/with 等无法静态分析的特性。避免 Deopt:保持函数参数类型一致、避免修改 hot 函数的 shape。← TurboFan::Deoptimize()
全链路源码索引速查
| 阶段 | 关键函数 | Chromium 源码位置 |
| DNS 解析 | HostResolver::Resolve() | net/dns/host_resolver.cc |
| TCP 连接 | TCPConnect() | net/socket/tcp_client_socket.cc |
| TLS 握手 | SSLClientSocket::Connect() | net/socket/ssl_client_socket.cc |
| HTML 解析 | HTMLParser::ProcessToken() | blink/core/html/html_document_parser.cc |
| CSSOM 构建 | CSSParser::ParseRule() | blink/core/css/css_parser.cc |
| Render Tree | ComputedStyle::Create() | blink/core/style/computed_style.cc |
| Layout | LayoutObject::Layout() | blink/core/layout/layout_object.cc |
| Paint | PaintController::DrawRecord() | blink/core/paint/paint_controller.cc |
| Composite | CompositingManager::Update() | blink/platform/graphics/compositing_manager.cc |
| V8 编译 | ScriptCompiler::Compile() | v8/src/compiler.cc |
| 微任务 | MicrotaskQueue::RunMicrotasks() | v8/src/microtask-queue.cc |
| 事件循环 | MessageLoop::Run() | base/message_loop.cc |