浏览器渲染管线:从导航到合成层的完整流程

导航、解析、渲染、回流重绘、合成层、事件循环,以及与 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 什么时候能执行。