浏览器渲染管线:从导航到合成层的完整流程
导航、解析、渲染、回流重绘、合成层、事件循环,以及与 React 的关系
完整流程
用户输入 URL
↓
1. 导航(Navigation)
DNS 解析 → TCP 三次握手 → TLS 握手 → HTTP 请求
↓
2. 响应(Response)
服务端返回 HTML(首字节 TTFB,首包 14KB)
↓
3. 解析(Parsing)
HTML → DOM 树
CSS → CSSOM 树
JS → AST → 字节码 → 执行
↓
4. 渲染(Render)
DOM + CSSOM → Render Tree → Layout → Paint → Composite
↓
5. 交互(Interaction)
用户点击/滚动 → 事件循环处理
1. 导航阶段
URL 输入
↓
DNS 解析:域名 → IP 地址(每个域名只解析一次,结果缓存)
↓
TCP 三次握手:SYN → SYN-ACK → ACK(建立连接)
↓
TLS 握手:协商加密方式(HTTPS 额外 1-2 个 RTT)
↓
HTTP 请求:浏览器发送 GET 请求
↓
TTFB(Time to First Byte):从请求到收到第一个字节的时间
↓
首包 14KB:TCP 慢启动,第一次只传 14KB
TCP 慢启动:
初始窗口:14KB(CWND)
收到 ACK → 翻倍(14KB → 28KB → 56KB...)
丢包 → 减半
目标:找到网络带宽的最大值
2. 解析阶段
HTML → DOM 树:
<div>
<p>Hello</p>
</div>
→ DOM 树:
document
html
head
body
div
p
"Hello"
CSS → CSSOM 树:
div { font-size: 16px }
p { color: red }
→ CSSOM:
div → { font-size: 16px }
p → { font-size: 16px, color: red } // 继承
关键规则:
<script> 阻塞 HTML 解析(等 JS 下载+执行完才继续)
<script async> 异步下载,下载完立即执行(执行时阻塞)
<script defer> 异步下载,HTML 解析完后按顺序执行(不阻塞解析)
<link rel="stylesheet"> 阻塞渲染(必须等 CSS 下载完)
CSS 不阻塞 HTML 解析,但阻塞 JS 执行
图片 不阻塞解析(懒加载)
Preload Scanner(预扫描器):
主线程在解析 HTML 时,预扫描器在后台扫描后续内容
提前下载 CSS / JS / 字体等高优先级资源
等主线程解析到时,资源可能已经在路上了
3. 渲染阶段(5 步)
DOM + CSSOM
↓
1. Style(样式计算)
合并 DOM 和 CSSOM → Render Tree
display: none 的节点不加入
visibility: hidden 的加入(占位但不可见)
↓
2. Layout(布局 / 回流)
计算每个节点的几何信息(位置、大小)
从根节点遍历,考虑 box model、viewport
↓
3. Paint(绘制 / 重绘)
将每个节点转换为实际像素
绘制文字、颜色、边框、阴影、图片
↓
4. Composite(合成)
多个层叠在一起,决定最终显示
GPU 加速的层在这里合成
↓
5. 显示到屏幕
4. 回流 vs 重绘
回流(Reflow)= Layout 重新计算:
触发条件:
- 改变窗口大小
- 修改元素尺寸(width/height/padding/margin/border)
- 添加/删除 DOM 元素
- 读取 offsetWidth/scrollTop(强制同步布局)
- font-size 改变
重绘(Repaint)= 重新绘制,不影响布局:
触发条件:
- 改变颜色
- 改变背景
- 改变 visibility
- 改变阴影
- 改变 outline
关系:回流必定触发重绘,重绘不一定触发回流
避免频繁回流:
// ❌ 逐个读写 DOM(触发多次回流)
el.style.width = '100px'
el.style.height = '100px'
el.style.margin = '10px'
// ✅ 批量读写(只触发一次回流)
const width = '100px'
const height = '100px'
const margin = '10px'
el.style.cssText = `width:${width};height:${height};margin:${margin}`
// ✅ 或者用 requestAnimationFrame
requestAnimationFrame(() => {
el.style.width = '100px'
el.style.height = '100px'
})
5. 合成层(Composite Layers)
某些属性会触发 GPU 合成,避免主线程重绘:
/* 创建新层的属性 */
transform: translateZ(0) /* 3D 变换 */
opacity: 0.5 /* 透明度 */
will-change: transform /* 提示浏览器 */
filter: blur(10px) /* 滤镜 */
video, canvas /* 原生元素 */
合成层的好处: GPU 直接合成,不走主线程的 Paint,滚动和动画更流畅。
坏处: 每个层占内存,不能滥用。
6. 事件循环
不是简单的宏任务/微任务二分法,而是多个队列,不同优先级:
不同的任务源 → 不同的任务队列
├── 交互队列(用户点击、键盘输入)← 最高优先级
├── 延时队列(setTimeout、setInterval)
├── 网络队列(fetch 回调)
├── IdleQueue(requestIdleCallback)
└── 微任务队列(Promise.then、queueMicrotask、MutationObserver)
每个渲染帧的执行顺序:
1. 从最高优先级队列取一个任务执行
2. 清空微任务队列(所有微任务执行完)
3. 如果到了渲染时间(16.67ms ≈ 60fps)→ 执行渲染(Layout → Paint → Composite)
4. 继续取下一个任务
为什么用户交互优先级最高: 用户点击如果延迟响应,体验极差。setTimeout 延迟几毫秒用户感知不到。
7. 跟 React 的关系
React 的时间切片利用事件循环的间隙做调度:
- 用 MessageChannel 插入一个宏任务
- 当前宏任务执行完 → 清空微任务 → 检查 React 任务
- 有 → 执行 React 的 fiber 调度(可中断)
- 没有 → 浏览器正常渲染
React 18 的 Concurrent Mode:
- 低优先级任务(数据预取)可以被打断
- 高优先级任务(用户点击)立即响应
- lanes 优先级模型在多队列系统上做更细粒度的调度
8. 性能优化关键点
1. 减少 DOM 节点数量 → 缩短 Layout 时间
2. 避免频繁回流 → 批量读写 DOM
3. 使用合成层 → GPU 加速动画(transform / opacity / will-change)
4. 首屏 14KB 内包含关键 CSS + HTML → 避免白屏
5. script 加 async/defer → 不阻塞 HTML 解析
6. 图片声明宽高 → 避免加载后回流
7. 用 requestAnimationFrame 做动画 → 跟浏览器渲染帧同步
8. 用 IntersectionObserver 做懒加载 → 不阻塞主线程
核心一句话
浏览器渲染管线 = 解析(DOM\+CSSOM)→ 样式计算 → 布局 → 绘制 → 合成。React Fiber 在 Layout 和 Paint 之间做调度,lanes 优先级决定哪个任务先执行,事件循环决定 React 什么时候能执行。