浏览器渲染原理
浏览器把 HTML/CSS/JS 变成你看到的页面,整体流程可以分为以下几个核心阶段。
完整渲染流水线
HTML/CSS/JS
↓
解析 HTML → DOM 树
解析 CSS → CSSOM 树
↓
合并 → Render Tree(渲染树)
↓
Layout(布局/回流)
↓
Paint(绘制/重绘)
↓
Composite(合成)
↓
显示到屏幕
一、解析阶段
构建 DOM 树
<!-- 原始 HTML -->
<html>
<body>
<div class="box">
<p>Hello</p>
</div>
</body>
</html>
浏览器逐字节读取 HTML,经过字节 → 字符 → Token → Node → DOM 的转化过程:
DOM Tree:
html
└─ body
└─ div.box
└─ p
└─ "Hello"
重要细节:
-
解析是边下载边解析,不等全部下载完
-
遇到
<script>会阻塞解析(因为 JS 可能修改 DOM) -
<script src="main.js"></script> <script async src="main.js"></script> <script defer src="main.js"></script>async和defer可以避免阻塞:
构建 CSSOM 树
div { font-size: 16px; }
.box { color: red; }
.box p { font-weight: bold; }
CSSOM Tree:
div
└─ font-size: 16px
.box
└─ color: red
└─ p
└─ font-weight: bold
重要细节:
- CSS 解析会阻塞渲染(有了 CSSOM 才能合并渲染树)
- CSS 不阻塞 DOM 解析,但阻塞 JS 执行(JS 可能读取样式)
- 所以 CSS 要放
<head>里,JS 放<body>底部
二、Render Tree(渲染树)
把 DOM 树和 CSSOM 树合并,只保留可见节点:
// 这些节点不会进入 Render Tree
display: none // 完全不渲染
<head>、<script> // 不可见元素
<!-- 注释 --> // 注释节点
// 这个会进入 Render Tree(虽然不可见,但占位置)
visibility: hidden // 占据空间,只是不可见
Render Tree:
body
└─ div.box (color: red, font-size: 16px)
└─ p (font-weight: bold)
└─ "Hello"
三、Layout(布局 / 回流)
计算每个节点的位置和大小:
// Layout 需要计算的信息
{
x: 0,
y: 0,
width: 1200,
height: 400
}
触发回流(Reflow)的操作:
// 改变几何属性 → 触发回流(代价最大!)
element.style.width = '200px';
element.style.height = '300px';
element.style.margin = '10px';
element.style.padding = '5px';
element.style.fontSize = '20px';
// 读取几何信息 → 强制同步回流(非常慢!)
const width = element.offsetWidth;
const height = element.offsetHeight;
const rect = element.getBoundingClientRect();
// 添加/删除 DOM → 触发回流
document.body.appendChild(newDiv);
回流优化技巧:
// ❌ 错误:每次修改都触发回流
element.style.width = '200px';
element.style.height = '300px';
element.style.margin = '10px';
// ✅ 正确:批量修改(只触发一次回流)
element.style.cssText = 'width: 200px; height: 300px; margin: 10px;';
// 或者
element.className = 'new-class';
// ❌ 错误:读写交替(触发多次强制同步回流)
const width1 = el1.offsetWidth; // 读 → 强制回流
el1.style.width = '100px'; // 写
const width2 = el2.offsetWidth; // 读 → 再次强制回流
el2.style.width = '200px'; // 写
// ✅ 正确:先读后写(只触发一次回流)
const width1 = el1.offsetWidth; // 读
const width2 = el2.offsetWidth; // 读
el1.style.width = '100px'; // 写
el2.style.width = '200px'; // 写
// ✅ 使用 DocumentFragment 批量插入 DOM
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // 操作 fragment,不触发回流
}
document.body.appendChild(fragment); // 只触发一次回流
四、Paint(绘制 / 重绘)
把每个节点绘制成像素,包括颜色、边框、阴影、文字等。
触发重绘(Repaint)的操作:
// 只改变外观,不影响布局 → 只触发重绘(比回流轻量)
element.style.color = 'red';
element.style.background = 'blue';
element.style.borderColor = 'green';
element.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
element.style.visibility = 'hidden';
回流 vs 重绘:
回流(Reflow):
几何属性变化 → Layout → Paint → Composite
代价:最大
重绘(Repaint):
外观属性变化 → Paint → Composite
代价:中等
合成(Composite):
transform/opacity → 只 Composite
代价:最小 ✅
五、Composite(合成)
浏览器把页面分成多个图层(Layer),分别绘制后再合成:
// 触发独立图层(GPU 加速)
transform: translateZ(0) // 强制提升为独立图层
will-change: transform // 提前告知浏览器
position: fixed // 固定定位
opacity < 1 + animation // 有动画的透明度
video、canvas、iframe // 自动提升
// 只触发 Composite 的属性(最高效!)
transform: translate(100px, 0)
transform: scale(1.5)
opacity: 0.5
动画最佳实践:
/* ❌ 触发回流的动画(卡顿)*/
.bad-animation {
transition: left 0.3s, top 0.3s; /* left/top 触发回流 */
}
/* ✅ 只触发 Composite 的动画(流畅)*/
.good-animation {
transition: transform 0.3s; /* transform 只走合成 */
will-change: transform; /* 提前提升图层 */
}
六、关键渲染路径优化
优化策略汇总
// 1. CSS 放 <head>,JS 放底部 或用 defer
<head>
<link rel="stylesheet" href="style.css"> // ✅
</head>
<body>
<!-- 内容 -->
<script defer src="app.js"></script> // ✅
</body>
// 2. 避免强制同步回流
// ❌
function animate() {
box.style.left = box.offsetLeft + 1 + 'px'; // 读写混合!
}
// ✅
let position = 0;
function animate() {
position += 1;
box.style.transform = `translateX(${position}px)`; // 用 transform
}
// 3. 使用 requestAnimationFrame 做动画
function animate() {
element.style.transform = `translateX(${x++}px)`;
requestAnimationFrame(animate); // 跟随屏幕刷新率,不丢帧
}
requestAnimationFrame(animate);
// 4. 复杂计算放 Web Worker(不阻塞主线程)
const worker = new Worker('heavy-task.js');
worker.postMessage({ data: bigData });
worker.onmessage = (e) => {
console.log('计算结果:', e.data);
};
// 5. 大列表用虚拟滚动
// 只渲染可见区域的 DOM 节点
// 推荐:react-window、vue-virtual-scroller
七、浏览器的优化机制
渲染队列(异步合批)
// 浏览器不会立即回流,而是把操作放入队列,统一处理
element.style.width = '100px'; // 进队列
element.style.height = '200px'; // 进队列
element.style.margin = '10px'; // 进队列
// 一次性触发回流 ✅
// 但读取几何属性会强制清空队列,立即回流
element.style.width = '100px'; // 进队列
const h = element.offsetHeight; // 强制清空队列,立即回流 ❌
element.style.height = '200px'; // 再进队列,再次回流