从一行 JSX 到屏幕像素:前端开发者必须懂的浏览器渲染管线

你写了一个 setState,React 更新了虚拟 DOM,然后呢?然后浏览器要经历解析、样式计算、布局、绘制、合成五个阶段,才能把像素画到屏幕上。理解这个过程,你才能真正理解"为什么这样做性能更好"。


一、先看一个故事

你的 test.html 里有这样一段代码:

js 复制代码
// 方式一:逐个插入(慢)
for (const task of data) {
    const item = document.createElement('li');
    item.innerText = task;
    oList.appendChild(item);  // 每次都触发页面重新渲染!
}

// 方式二:批量插入(快)
const fragment = document.createDocumentFragment();
for (const task of data) {
    const item = document.createElement('li');
    fragment.appendChild(item);  // 在内存中,不触发渲染
}
oList.appendChild(fragment);     // 一次性挂载,只渲染一次

为什么方式二更快?"渲染"到底发生了什么?

这篇文章把这个过程从头到尾讲清楚。


二、完整流程:五层漏斗

css 复制代码
你写的代码
    │
    ▼
┌─────────────────────────────────────────┐
│  阶段一:网络 & 解析                      │
│  HTML/CSS/JS 字节 → 解码 → Token → 树    │
│  产出:DOM 树 + CSSOM 树                  │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  阶段二:样式计算                         │
│  DOM + CSSOM → 合并 → Render Tree        │
│  产出:渲染树(去掉了 display:none 等)      │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  阶段三:Layout(布局/重排)              │
│  计算每个元素在页面上的精确位置和大小         │
│  产出:盒模型数据(x, y, w, h, margin...) │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  阶段四:Paint(绘制/重绘)               │
│  把每个元素画出来(颜色、边框、阴影、文字)    │
│  产出:位图(像素数据)                     │
└─────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────┐
│  阶段五:Composite(合成)                │
│  把多个图层叠在一起,输出到屏幕              │
│  产出:屏幕上的像素 ✨                     │
└─────────────────────────────────────────┘

三、阶段一:解析 --- 从字节到树

3.1 HTML → DOM 树

css 复制代码
HTML 字节流
  → 解码成字符(UTF-8)
  → Tokenizer(词法分析器):切分成 token
     <div class="box">Hello</div>
       ↓
     [StartTag:div] [Attr:class="box"] [Text:Hello] [EndTag:div]
  → 构建 DOM 节点
  → 连接成 DOM 树

DOM 树是内存中的一棵树,它描述了文档的结构,但没有任何样式信息

3.2 CSS → CSSOM 树

遇到 <style><link> 时,浏览器解析 CSS,构建 CSSOM 树。

复制代码
CSS 文本
  → 解析选择器、属性、值
  → 计算继承关系
  → 构建 CSSOM 树

CSSOM 记录了每个元素最终应用的所有 CSS 属性(包括继承来的和浏览器默认的)。

⚠️ CSS 解析会阻塞渲染 ------浏览器不会开始 Layout,因为后面的 CSS 可能会覆盖前面的样式。但它不阻塞 DOM 构建

3.3 JavaScript 执行

遇到 <script> 时:

⚠️ JS 执行会阻塞 HTML 解析 ------因为 JS 可能 document.write() 改变 HTML 结构,所以解析器要停下来等 JS 跑完。

这也是为什么现代前端工程会把 <script> 放在 <body> 底部或用 async/defer


四、阶段二:样式计算 --- Render Tree 的诞生

markdown 复制代码
DOM 树                        CSSOM 树
    │                              │
    └──────────────┬───────────────┘
                   ▼
            Render Tree(渲染树)

渲染树不是 DOM 的简单复制,它会过滤掉不可见的元素:

原始 DOM 在渲染树中?
<head><script><meta> ❌ 不可见,不存在
display: none 的元素 ❌ 不可见,不存在
visibility: hidden 的元素 ✅ 在(占位,只是透明)
opacity: 0 的元素 ✅ 在
伪元素 ::before / ::after ✅ 在(虽然不是 DOM 节点)

五、阶段三:Layout --- 最贵的步骤

Layout(也叫 Reflow / 重排)做一件事:计算每个渲染树节点的几何信息

css 复制代码
对于渲染树中的每个可见节点:
  → 计算宽度(width)
  → 计算高度(height)
  → 计算位置(x, y 坐标)
  → 计算盒模型(margin, padding, border)
  → 处理文本换行

一个计算结果会影响周围的元素(比如一个元素变宽了,旁边的元素位置得跟着移动)。所以一个节点的 Layout 可能触发连锁反应------这也正是 Layout 昂贵的原因。

什么操作触发 Layout?

css 复制代码
增删 DOM 节点           → Layout ✅
修改 width / height     → Layout ✅
修改 margin / padding   → Layout ✅
修改 left / top         → Layout ✅
修改 font-size          → Layout ✅(文字大小影响容器高度)
读取 offsetHeight 等    → Layout ✅(浏览器强制同步 Layout 以返回准确值)

六、阶段四:Paint --- 把它画出来

Layout 确定了位置和大小 ,Paint 确定长什么样

css 复制代码
对于每个元素:
  → 背景色(background-color)
  → 文字颜色和字体(color, font-family)
  → 边框(border)
  → 阴影(box-shadow, text-shadow)
  → 轮廓(outline)

浏览器会把页面分解成多个图层(Layers),每个图层独立进行光栅化(把矢量信息转成像素点)。

什么操作只触发 Paint(不触发 Layout)?

css 复制代码
修改 color              → Paint  ✅(Layout ❌)
修改 background-color   → Paint  ✅
修改 box-shadow         → Paint  ✅
修改 visibility         → Paint  ✅

七、阶段五:Composite --- 合成为最终画面

浏览器有一个合成器线程(Compositor Thread),它把多个图层按照 z-index 顺序叠在一起,生成最终的位图,然后提交给 GPU 显示到屏幕。

css 复制代码
图层 A(背景)
图层 B(内容)      →  Compositor → 合并 → GPU → 屏幕
图层 C(弹窗)

为什么这是最快的阶段

Composite 是唯一在 GPU 上执行 的阶段,而且不依赖主线程。你可以在 JS 主线程忙的时候,仍然有流畅的 CSS 动画。

什么操作只触发 Composite(最快!)

css 复制代码
修改 transform          → 只 Composite ✅(Layout ❌, Paint ❌)
修改 opacity            → 只 Composite ✅
CSS 动画(transform)   → 只 Composite ✅

这也是为什么"用 transform 做动画"是性能最佳实践------它跳过了 Layout 和 Paint 两个最贵的步骤。


八、一张总览表

你改了什么属性 Layout Paint Composite 代价
widthheight 🔴 最高
marginpadding 🔴 最高
增/删 DOM 节点 🔴 最高
font-size 🔴 最高
background-color 🟡 中等
color 🟡 中等
box-shadow 🟡 中等
visibility 🟡 中等
transform 🟢 最低
opacity 🟢 最低
cursor 🟢 无

九、React 做了什么

理解了浏览器渲染管线,你就能理解 React 的很多设计选择:

9.1 虚拟 DOM + Reconciliation

复制代码
你调用三次 setState
  → React 不立即操作 DOM
  → 在虚拟 DOM 中 diff
  → 算出最小变更集
  → 一次性应用到真实 DOM
  → 浏览器只触发一次 Layout → Paint → Composite

这就是你 test.html 中 DocumentFragment 做的事情,但 React 把它自动化了。

9.2 批处理(Batching)

React 18 之后,所有 setState 调用都会自动批处理------不管它们在事件处理函数、setTimeout、还是 Promise 中。这确保了一次渲染帧中只触发一次浏览器渲染管线

9.3 React Fragment(<></>

减少无意义的 DOM 层级 → 减少 Layout 中需要计算的节点数 → 更快。


十、一句话总结

复制代码
浏览器从你写的代码到显示画面,经历了:
解析(构建树)→ 样式计算(合并树)→ Layout(算位置)→ Paint(画出来)→ Composite(叠图层)

Layout 最贵,Paint 次之,Composite 最便宜。
React 的所有性能优化(虚拟 DOM、批处理、Fragment)本质上都是在减少 Layout 和 Paint 的次数。
相关推荐
恒拓高科WorkPlus1 小时前
企业级内网即时通讯建设模型:BeeWorks内网IM四层架构
前端
前端小李子2 小时前
前端环境变量裸奔?我用 EnvShield 给它穿了件防弹衣
前端
youtootech2 小时前
HarmonyOS《柚兔学伴》项目实战25-我的页面、Web 嵌入与项目总结
前端·华为·harmonyos
小林ixn2 小时前
从零到一理解 React 父子组件通信:手写一个 Todo 应用带你彻底搞懂单向数据流
前端·javascript·react.js
醇氧4 小时前
CountDownLatch / CyclicBarrier / Semaphore 面试高频问答清单
前端·面试·职场和发展
qetfw5 小时前
MXU:Tauri 2 + React 的 MaaFramework 跨平台 GUI 源码
前端·python·react.js·前端框架·开源项目·效率工具
甲维斯5 小时前
我要开始吹牛逼了!Kimi K3 “宇宙无敌”!
前端·人工智能
a1117765 小时前
微光小屋-前端养成小游戏 开源项目
前端