浏览器渲染流程与重排重绘
一、关键渲染路径 (Critical Rendering Path)
1.1 渲染流程概览
HTML → DOM Tree
→ Render Tree → Layout → Paint → Composite
CSS → CSSOM ↗- 解析 HTML → 构建 DOM 树
- 解析 CSS → 构建 CSSOM 树
- 合并 → 生成 Render Tree (渲染树)
- Layout (布局/重排) → 计算元素几何信息
- Paint (绘制/重绘) → 填充像素
- Composite (合成) → 合成图层,显示到屏幕
1.2 详细流程
┌─────────────────────────────────────────────────────────────┐
│ 1. 构建 DOM │
│ HTML → Bytes → Characters → Tokens → Nodes → DOM │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. 构建 CSSOM │
│ CSS → Bytes → Characters → Tokens → Nodes → CSSOM │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. 构建 Render Tree │
│ - 只包含可见元素 │
│ - 不包含 display:none / <head> / <script> │
│ - visibility:hidden 会包含 (占位但不可见) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Layout (Reflow) │
│ - 计算每个元素的精确位置和大小 │
│ - 自上而下遍历渲染树 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Paint │
│ - 将元素转换为屏幕上的实际像素 │
│ - 分层绘制 (Layers) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 6. Composite │
│ - GPU 合成各图层 │
│ - 最终显示到屏幕 │
└─────────────────────────────────────────────────────────────┘二、重排 (Reflow) 与重绘 (Repaint)
2.1 基本概念
| 类型 | 说明 | 性能影响 |
|---|---|---|
| 重排 | 元素几何属性变化,需重新计算布局 | 高 |
| 重绘 | 元素外观变化,不影响布局 | 中 |
| 合成 | 只改变合成层属性 | 低 |
重排必定引起重绘,但重绘不一定引起重排
2.2 触发重排的操作
javascript
// 1. 几何属性变化
element.style.width = '100px';
element.style.height = '100px';
element.style.padding = '10px';
element.style.margin = '10px';
element.style.border = '1px solid';
// 2. 位置属性变化
element.style.top = '10px';
element.style.left = '10px';
element.style.position = 'absolute';
// 3. 显示属性变化
element.style.display = 'none';
element.style.display = 'block';
// 4. 字体属性变化
element.style.fontSize = '16px';
element.style.fontWeight = 'bold';
// 5. 内容变化
element.innerText = 'new text';
element.innerHTML = '<span>new</span>';
// 6. 窗口大小变化
window.resize 事件
// 7. 读取某些属性 (强制同步布局)
element.offsetTop / offsetLeft / offsetWidth / offsetHeight
element.scrollTop / scrollLeft / scrollWidth / scrollHeight
element.clientTop / clientLeft / clientWidth / clientHeight
element.getBoundingClientRect()
getComputedStyle(element)2.3 触发重绘的操作
javascript
// 只触发重绘,不触发重排
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden'; // 元素仍占位
element.style.outline = '1px solid';
element.style.boxShadow = '0 0 10px';2.4 只触发合成的操作
javascript
// 性能最佳,只触发 Composite
element.style.transform = 'translateX(100px)';
element.style.opacity = '0.5';三、渲染优化策略
3.1 减少重排次数
javascript
// ❌ 多次重排
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
// ✅ 批量修改 - cssText
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// ✅ 批量修改 - class
element.className = 'new-style';
// ✅ 使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('div');
fragment.appendChild(item);
}
container.appendChild(fragment); // 只触发一次重排3.2 避免强制同步布局
javascript
// ❌ 强制同步布局 (读写交错)
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = box.offsetWidth + 'px'; // 每次循环都强制布局
}
// ✅ 先读后写
const width = box.offsetWidth; // 读
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = width + 'px'; // 写
}3.3 使用 requestAnimationFrame
javascript
// ❌ 直接操作
element.style.left = left + 'px';
element.style.top = top + 'px';
// ✅ 使用 RAF 批量更新
requestAnimationFrame(() => {
element.style.left = left + 'px';
element.style.top = top + 'px';
});3.4 离线 DOM 操作
javascript
// ✅ 先隐藏,操作完再显示
element.style.display = 'none';
// 执行多次 DOM 操作...
element.style.display = 'block';
// ✅ 使用克隆节点
const clone = element.cloneNode(true);
// 在克隆节点上操作...
element.parentNode.replaceChild(clone, element);3.5 使用 transform 代替位置属性
javascript
// ❌ 触发重排
element.style.left = x + 'px';
element.style.top = y + 'px';
// ✅ 只触发合成
element.style.transform = `translate(${x}px, ${y}px)`;四、图层与合成
4.1 创建新图层的条件
css
/* 以下属性会创建新的合成层 */
.layer {
transform: translateZ(0); /* 3D 变换 */
will-change: transform; /* will-change */
opacity: 0.9; /* opacity < 1 */
filter: blur(5px); /* filter */
position: fixed; /* fixed 定位 */
/* video / canvas / iframe 等 */
}4.2 图层优化
javascript
// 查看页面图层
// Chrome DevTools → More tools → Layers
// 注意:图层过多会消耗内存
// 只对频繁变化的元素创建新图层五、高频面试题
Q1: 什么是重排和重绘?如何避免?
答案:
- 重排: 元素几何属性变化,重新计算布局
- 重绘: 元素外观变化,重新绘制
- 避免方法:
- 批量修改样式
- 使用 transform 代替位置属性
- 避免读写交错导致强制同步布局
- 使用 RAF 批量更新
Q2: 为什么操作 DOM 慢?
答案:
- DOM 是渲染引擎的对象,JS 是 JS 引擎的对象,跨引擎通信有性能开销
- DOM 操作可能触发重排重绘
- 频繁操作会导致多次渲染
Q3: 浏览器渲染流程?
答案:
- 解析 HTML 构建 DOM 树
- 解析 CSS 构建 CSSOM 树
- 合并生成 Render Tree
- Layout 计算布局
- Paint 绘制
- Composite 合成显示
Q4: visibility:hidden 和 display:none 的区别?
| 属性 | visibility:hidden | display:none |
|---|---|---|
| 占位 | 是 | 否 |
| 重排 | 否 | 是 |
| 子元素 | 可单独设置 visible | 不显示 |
| 事件 | 不响应 | 不响应 |