从URL到像素 — 浏览器渲染全链路图解

当你输入一个网址按下回车,到屏幕上出现画面,中间发生了什么?

Chromium 源码映射 SVG 流程图 生活类比 IDE 代码窗 性能优化
一、从URL到像素的完整旅程
一次页面加载涉及网络、解析、渲染三大阶段,下图画出完整链路与每一步对应的 Chromium 源码入口。
网络阶段 解析阶段 渲染阶段 URL 输入 用户键入 DNS 解析 ← HostResolver TCP 三次握手 ← TCPConnect() TLS 握手 ← SSLClientSocket HTTP 请求 ← URLRequest 服务器响应 ← CommitNavigation 网络耗时:DNS ~20ms + TCP ~30ms + TLS ~40ms + TTFB ~50ms ≈ 140ms HTML 下载+解析 ← HTMLParser::Parse 构建 DOM Tree ← HTMLDocumentParser CSSOM 构建 ← CSSParser::ParseRule Render Tree 构建 ← ComputedStyle::Create <script> 阻塞解析 ← HTMLParser::PauseParsing() — 直到 JS 执行完毕 解析耗时:DOM ~50ms + CSSOM ~20ms + Render Tree ~10ms ≈ 80ms Layout 布局 ← LayoutObject::Layout() Paint 绘制 ← PaintController::Draw Composite 合成 ← CompositingManager::Update 像素到屏幕 GPU Raster → Display 渲染耗时:Layout ~15ms + Paint ~20ms + Composite ~5ms ≈ 40ms 关键时间指标 FP (First Paint) ~150ms — 首个像素绘制到屏幕 ← LocalFrameView::PerformLayout FCP (First Contentful Paint) ~200ms — 首次有意义内容出现 ← PaintTimingDetector::NotifyFirstPaint LCP (Largest Contentful Paint) ~400ms — 最大内容元素渲染完成 ← LCPObserver::OnLargestContentfulPaint TTI (Time to Interactive) ~600ms — 页面可交互 ← InteractiveDetector::OnInteractive TBT (Total Blocking Time) 长任务阻塞总和 ← LongTaskDetector::ReportLongTask CLS (Cumulative Layout Shift) 布局偏移累计 ← LayoutShiftTracker::NotifyShift
图 1-1:从 URL 到像素的完整管线 — 每一步标注了 Chromium 源码入口与典型耗时
🏠 生活类比:盖房子全流程

URL = 地址 — 你知道要去哪

DNS = 114查号台 — 先查本地通讯录,没有再一级级往上问

TCP = 打电话建立连接 — "喂?能听到吗?" "能!" "那我说了"

TLS = 加密通话 — 先交换密钥,再说正事防窃听

HTML = 建筑蓝图 — 哪里是墙、哪里是门

CSSOM = 装修方案 — 墙什么色、门多宽

Render Tree = 去掉隐藏房间的蓝图 — display:none 的不画

Layout = 量尺寸 — 算出每面墙的确切位置和大小

Paint = 刷漆 — 按尺寸给每面墙上色

Composite = 拼图层 — 把多层透明画纸叠在一起贴到屏幕

Explorer
▾ chromium/
▾ browser/
navigation_handle.cc
▾ renderer/
html_parser.cc
render_pipeline.cc
layout_object.cc
paint_controller.cc
▾ v8/
execution.cc
// 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 握手
在拿到任何数据之前,浏览器需要先找到服务器、建立连接、加密通道。
DNS 递归解析流程 浏览器 DNS 缓存 ← HostCache::Lookup() miss 操作系统 DNS 缓存 ← getaddrinfo() miss 路由器 DNS ← 路由器缓存 miss ISP 递归 DNS 服务器 ← HostResolver::Resolve() miss 根 DNS 服务器 返回 .com 的 NS TLD DNS (.com) 返回 example.com 的 NS 权威 DNS 服务器 返回 A 记录 93.184.216.34 hit! → 缓存到各级 ~0ms ~1ms ~5ms ~10ms ~20ms
图 2-1:DNS 递归解析 — 从浏览器缓存逐级到权威 DNS,← net::HostResolver::Resolve()
TCP 三次握手 客户端 ← TCPConnect() 服务器 SYN seq=100 "我能打给你吗?" SYN+ACK seq=300 ack=101 "可以,我也准备好了!" ACK ack=301 "那我开始说了!" ~10ms ~20ms ~30ms
图 2-2:TCP 三次握手 — SYN → SYN+ACK → ACK,← TCPConnect() 三次握手入口
TLS 1.3 握手(1-RTT) 客户端 ← SSLClientSocket 服务器 ClientHello + Key Share "我支持这些加密套件,这是我的公钥" ServerHello + Certificate + Key Share "用这个套件,这是我的证书和公钥" Finished (加密) "密钥确认,开始加密通信" Application Data 🔒 加密传输 1-RTT
图 2-3:TLS 1.3 握手 — 仅需 1-RTT,← SSLClientSocket::Connect() TLS握手入口
📞 生活类比

DNS = 114查号台 — 先查本地通讯录(浏览器缓存),没有再一级级问(路由器→ISP→根→TLD→权威)

TCP = 打电话 — SYN="我能打给你吗?",SYN+ACK="可以!",ACK="那我开始了"

TLS = 加密通话 — 先交换密钥确认身份,再说正事防窃听。TLS 1.3 只需 1 次来回

Explorer
▾ net/
▸ dns/
host_resolver.cc
▸ socket/
ssl_client_socket.cc
tcp_client_socket.cc
▸ url_request/
url_request.cc
// 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?
HTML 解析管线:Bytes → Characters → Tokens → Nodes → DOM Bytes 字节流 3C 64 69 76 3E <div> 的 UTF-8 编码 Characters <div><p>Hello </p></div> 分词 Tokens StartTag:div StartTag:p Char:"Hello" EndTag:p EndTag:div ← HTMLTokenizer::NextToken() 建树 DOM Tree Document div p "Hello" ← HTMLParser::ProcessToken()
图 3-1:HTML 解析管线 — 字节流经编码、分词、建树三步变成 DOM Tree
DOM + CSSOM → Render Tree 合成过程 DOM Tree html body div.box div.hide display:none ❌ p "Hello" CSSOM Tree body font:14px .box { color:red } .hide { display:none } p { margin:8px } ← CSSParser::ParseRule() + Render Tree RenderBody RenderBox(.box) color:red font:14px RenderP("Hello") margin:8px ← ComputedStyle::Create() <script> 标签如何阻塞解析 HTML 解析中... 遇到 <script> 暂停解析, 下载+执行JS 恢复解析 ← async: 并行下载, 下载完阻塞 ← defer: 并行下载, DOMContentLoaded前执行 ← 无属性: 完全阻塞! ← HTMLParser::PauseParsing() / ResumeParsing()
图 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 解析。

Explorer
▾ third_party/blink/
▸ core/html/
▸ core/css/
html_document_parser.cc
html_tokenizer.cc
css_parser.cc
computed_style.cc
// 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(合成)三大步骤把页面渲染到屏幕。
Layout 阶段 — 计算每个元素的确切位置和尺寸 Render Tree .box { width: 50% } .box { padding: 20px } .box { margin: 10px } ← ComputedStyle 布局计算 容器宽 800px → 50% = 400px + padding 20px×2 = 440px + margin 10px×2 = 460px ← LayoutObject::Layout() Layout Tree(像素坐标) margin: 10px padding: 20px content: 400px × 24px 布局类型 BFC (块级格式化上下文) 垂直排列、margin 合并 ← LayoutBlockFlow IFC (内联格式化上下文) 水平排列、baseline 对齐 ← LayoutInline Flex / Grid 布局 弹性/网格分配空间 ← LayoutFlexibleBox / LayoutGrid
图 4-1:Layout 阶段 — CSS 属性计算出像素位置,← LayoutObject::Layout()
Paint 阶段 — 按顺序生成绘制指令 绘制顺序 (Paint Order) 1. Background 背景色/背景图 2. Float 浮动内容 3. Foreground 内联内容/文字 4. Outline 轮廓线 Paint Records (绘制指令列表) drawRect(10, 10, 460, 280, #fff) → drawText("Hello", 30, 100) → drawBorder(10, 10, 460, 280, 1px solid #ccc) ← PaintController::DrawRecord() Rasterization 光栅化 — 矢量指令 → 像素位图 GPU 加速:Skia → GPU Tile Rasterization ← RasterBufferProvider::RasterizeSource()
图 4-2:Paint 阶段 — 按特定顺序生成绘制指令,再光栅化为像素位图
Composite 阶段 — 多图层合成到屏幕 Layer 1: 背景 background: #fafaf8 Layer 2: 内容 "Hello World" opacity: 0.8 Layer 3: 浮动元素 transform: translateY will-change: transform 屏幕 GPU ← CompositingManager::Update() — GPU 纹理上传 → 合成 → VSync 同步显示 提升为独立合成层的条件 transform / opacity 动画 | will-change | position:fixed | video/canvas/iframe | 硬件加速的 CSS filter
图 4-3:Composite 阶段 — 多图层由 GPU 合成,← CompositingManager::Update()
Reflow vs Repaint vs Composite-Only — 性能影响对比 Reflow (回流) width/height/padding/margin Layout 重新计算 Paint 重新绘制 Composite 重新合成 性能消耗最大 ⚠️ ← StyleChangeType::kLayout 整个管线重跑 例: el.style.width = '200px' Repaint (重绘) color/background/visibility Layout 跳过 ✅ Paint 重新绘制 Composite 重新合成 性能消耗中等 ← StyleChangeType::kPaint 跳过 Layout 例: el.style.color = 'red' Composite-Only transform / opacity Layout 跳过 ✅ Paint 跳过 ✅ Composite 仅合成 性能消耗最小 🚀 ← StyleChangeType::kComposite 仅 GPU 合成 例: el.style.transform = 'translateX(10px)'
图 4-4:Reflow / Repaint / Composite-Only — 不同 CSS 属性变更触发不同级别的渲染更新,← StyleChangeType 判断变更级别
🎨 生活类比:画布作画

Layout = 画草图量尺寸 — 先用铅笔画出每个元素的位置和大小

Paint = 按草图上色 — 按背景→内容→轮廓的顺序涂颜色

Composite = 把多个透明图层叠在一起 — 每层单独画好,最后叠在一起看

Reflow = 改结构要重画 — 墙要拆了重建,草图、上色全重来

Repaint = 只换颜色不改建 — 草图不变,只重新上色

Composite-Only = 只移动便利贴不动底层 — 便利贴换个位置,底层画好的完全不动

Pro 动画优先使用 transformopacity,只触发 Composite-Only 路径,可达 60fps。避免在动画中使用 width/height/top/left,这些会触发 Reflow。
Explorer
▾ blink/renderer/
▸ core/layout/
layout_object.cc
▸ core/paint/
paint_controller.cc
▸ platform/graphics/
compositing_manager.cc
// 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?事件循环如何调度同步/异步任务?理解这一点才能写出高性能的前端代码。
V8 引擎编译管线 JS 源码 let x = 1 + 2; ← ScriptCompiler AST 抽象语法树 VariableDecl ← Parser::Parse() Ignition 字节码 LdaSmi [1] AddSmi [2] ← Ignition::Execute() 热点 TurboFan 优化编译 内联缓存 + 类型反馈 ← TurboFan::Optimize() 机器码 CPU 直接执行 ← 最快路径 🚀 Deoptimization — 类型假设失败时回退到字节码 ← TurboFan::Deoptimize() 执行速度:字节码 ~10x 慢于机器码 | TurboFan 优化后接近 C++ 性能 | Deopt 回退代价大 → 避免多态
图 5-1:V8 编译管线 — Source → AST → Ignition字节码 → (热点) → TurboFan优化 → 机器码,← v8::ScriptCompiler::Compile()
Event Loop 事件循环 — 执行顺序全解 Call Stack 调用栈 console.log('1') ← 同步 main() 全局执行 ← 执行中的代码 Microtask Queue 微任务 Promise.then(cb) ← 3 MutationObserver ← MicrotaskQueue::Run() Macrotask Queue 宏任务 setTimeout(cb) ← 2 setInterval / I/O ← BlinkScheduler::PostTask() requestAnimationFrame rAF cb ← 4 ← 渲染前执行 Event Loop 循环 ① 清空调用栈 ② 清空微任务 ③ rAF 回调 ④ 渲染管线 ⑤ 取一个宏任务 ⑥ 清空微任务 循环往复 ♻️ console.log('1'); // 同步 → 立即输出 1 setTimeout(() => log('2')); // Macro → 最后输出 2 Promise.resolve().then(() => log('3')); // Micro → 输出 3 输出顺序: 1 → 3 → render → 2 ← MicrotaskQueue::RunMicrotasks()
图 5-2:Event Loop 循环 — 同步→微任务→rAF→渲染→宏任务,← Chromium MessageLoop

async/await 如何映射到微任务

async/await 是 Promise 的语法糖。每个 await 之后的代码,等价于放入 .then() 的微任务回调。

写法等价形式执行类型
await promisepromise.then(后续代码)微任务
async function返回 Promise同步入口+微任务续延
Promise.then()直接注册微任务微任务
setTimeout()定时器到期后入宏任务队列宏任务
🏢 生活类比:办公室工作

Call Stack = 办公桌 — 正在处理的文件,一次只能做一件

Microtask = 领导紧急批示 — 必须马上办,办完手头的文件前先处理

Macrotask = 收件箱 — 办完所有紧急批示后,从收件箱取一件

rAF = 办完文件先去给领导汇报 — 渲染前汇报,汇报完再回来看收件箱

V8 = 翻译官 — 把 JS 翻译成 CPU 能懂的指令,热点的部分翻译成最高效的版本

Explorer
▾ v8/src/
execution.cc
microtask-queue.cc
▾ chromium/base/
message_loop.cc
▾ chromium/renderer/
scheduler.cc
// 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 TreeComputedStyle::Create()blink/core/style/computed_style.cc
LayoutLayoutObject::Layout()blink/core/layout/layout_object.cc
PaintPaintController::DrawRecord()blink/core/paint/paint_controller.cc
CompositeCompositingManager::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