一、初级境界:理解基本流程
1.1 渲染流程概览
URL 输入
↓
DNS 解析
↓
建立 TCP 连接
↓
发送 HTTP 请求
↓
接收 HTML
↓
解析 HTML → DOM 树
↓
解析 CSS → CSSOM 树
↓
合并 → Render 树
↓
布局(Layout)
↓
绘制(Paint)
↓
合成(Composite)
↓
显示页面
1.2 关键步骤详解
步骤 1:构建 DOM 树
<!DOCTYPE html>
<html>
<head>
<title>Page</title>
</head>
<body>
<div id="app">
<h1>Hello</h1>
<p>World</p>
</div>
</body>
</html>
浏览器解析成 DOM 树:
Document
└── html
├── head
│ └── title
│ └── "Page"
└── body
└── div#app
├── h1
│ └── "Hello"
└── p
└── "World"
步骤 2:构建 CSSOM 树
body { font-size: 16px; }
#app { color: blue; }
h1 { font-size: 24px; }
解析成 CSSOM 树:
body { font-size: 16px }
└── #app { color: blue }
└── h1 { font-size: 24px }
步骤 3:合并成 Render 树
DOM + CSSOM = Render 树
RenderObject(body)
└── RenderObject(div#app)
├── RenderObject(h1) { font-size: 24px, color: blue }
└── RenderObject(p) { font-size: 16px, color: blue }
步骤 4:布局(Layout)
计算每个元素的位置和大小:
h1: { x: 0, y: 0, width: 800, height: 24 }
p: { x: 0, y: 24, width: 800, height: 16 }
步骤 5:绘制(Paint)
将元素绘制成像素:
绘制 h1 的背景
绘制 h1 的文字
绘制 p 的背景
绘制 p 的文字
步骤 6:合成(Composite)
将多个图层合成最终图像。
二、中级境界:理解重排和重绘
2.1 重排(Reflow)
定义:元素的几何属性变化,需要重新计算布局。
触发条件: - 添加/删除元素 - 修改元素尺寸(width、height、padding、margin) - 修改元素位置(top、left) - 修改字体大小 - 窗口大小变化 - 读取某些属性(offsetWidth、scrollTop)
示例:
// ❌ 触发重排
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
// 每次修改都触发一次重排,共 3 次
优化:
// ✅ 批量修改
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// 或使用 class
element.className = 'new-style';
2.2 重绘(Repaint)
定义:元素的外观变化,但不影响布局。
触发条件: - 修改颜色(color、background-color) - 修改可见性(visibility) - 修改边框样式(border-style)
示例:
// 只触发重绘,不触发重排
element.style.color = 'red';
element.style.backgroundColor = 'blue';
2.3 重排 vs 重绘
| 操作 | 重排 | 重绘 | 性能 |
|---|---|---|---|
| 修改 width | ✅ | ✅ | 慢 |
| 修改 color | ❌ | ✅ | 快 |
| 修改 transform | ❌ | ❌ | 最快 |
性能对比:
重排 + 重绘 > 重绘 > 合成
三、高级境界:性能优化
3.1 避免强制同步布局
问题代码:
// ❌ 强制同步布局
for (let i = 0; i < 100; i++) {
const width = element.offsetWidth; // 读取,触发布局
element.style.width = width + 10 + 'px'; // 写入,触发重排
}
// 读写交替,触发 100 次重排
优化:
// ✅ 批量读取,批量写入
const width = element.offsetWidth; // 一次读取
for (let i = 0; i < 100; i++) {
element.style.width = width + 10 * i + 'px';
}
// 只触发 1 次重排
3.2 使用 transform 代替 top/left
问题代码:
// ❌ 触发重排
element.style.left = '100px';
element.style.top = '100px';
优化:
// ✅ 只触发合成
element.style.transform = 'translate(100px, 100px)';
原因:transform 在合成层处理,不触发重排和重绘。
3.3 使用 will-change 提示浏览器
.animated {
will-change: transform, opacity;
}
作用:告诉浏览器这个元素会变化,提前优化。
注意:不要滥用,会消耗内存。
3.4 使用 requestAnimationFrame
// ❌ 使用 setTimeout
setTimeout(() => {
element.style.left = '100px';
}, 16);
// ✅ 使用 requestAnimationFrame
requestAnimationFrame(() => {
element.style.left = '100px';
});
优势: - 与浏览器刷新率同步 - 页面不可见时自动暂停 - 性能更好
3.5 虚拟滚动
问题:渲染 10000 个列表项,性能差。
// ❌ 渲染所有项
for (let i = 0; i < 10000; i++) {
list.appendChild(createItem(i));
}
优化:只渲染可见区域
// ✅ 虚拟滚动
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = visibleStart + visibleCount;
for (let i = visibleStart; i < visibleEnd; i++) {
list.appendChild(createItem(i));
}
四、资深境界:深入原理
4.1 渲染层合成
图层类型:
1. 普通图层:默认图层 2. 合成图层:独立图层,GPU 加速
创建合成图层的条件:
/* 1. 3D transform */
transform: translateZ(0);
transform: translate3d(0, 0, 0);
/* 2. will-change */
will-change: transform;
/* 3. video、canvas、iframe */
/* 4. position: fixed */
position: fixed;
/* 5. opacity 动画 */
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
优势: - GPU 加速 - 不影响其他图层 - 动画更流畅
劣势: - 消耗内存 - 图层过多反而变慢
4.2 关键渲染路径优化
目标:尽快显示首屏内容。
优化策略:
1. 减少关键资源数量
<!-- ❌ 阻塞渲染 -->
<link rel="stylesheet" href="style.css">
<script src="app.js"></script>
<!-- ✅ 异步加载 -->
<link rel="stylesheet" href="style.css" media="print" onload="this.media='all'">
<script src="app.js" defer></script>
2. 减少关键资源大小
# 压缩 CSS
cssnano style.css -o style.min.css
# 压缩 JS
terser app.js -o app.min.js
# Gzip 压缩
gzip -9 style.min.css
3. 缩短关键路径长度
<!-- ❌ 串行加载 -->
<link rel="stylesheet" href="a.css">
<link rel="stylesheet" href="b.css">
<!-- ✅ 内联关键 CSS -->
<style>
/* 首屏关键样式 */
</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
4.3 渲染性能指标
FCP(First Contentful Paint):首次内容绘制
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('FCP:', entry.startTime);
}
}).observe({ entryTypes: ['paint'] });
LCP(Largest Contentful Paint):最大内容绘制
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.startTime);
}
}).observe({ entryTypes: ['largest-contentful-paint'] });
FID(First Input Delay):首次输入延迟
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
}
}).observe({ entryTypes: ['first-input'] });
CLS(Cumulative Layout Shift):累积布局偏移
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('CLS:', entry.value);
}
}).observe({ entryTypes: ['layout-shift'] });
五、面试高频问题
5.1 从输入 URL 到页面显示发生了什么?
完整流程:
1. DNS 解析:域名 → IP 2. TCP 连接:三次握手 3. 发送 HTTP 请求 4. 服务器处理请求 5. 返回 HTTP 响应 6. 浏览器解析 HTML 7. 构建 DOM 树 8. 解析 CSS,构建 CSSOM 树 9. 合并成 Render 树 10. 布局(Layout) 11. 绘制(Paint) 12. 合成(Composite) 13. 显示页面
5.2 重排和重绘的区别?
重排: - 元素几何属性变化 - 需要重新计算布局 - 性能开销大
重绘: - 元素外观变化 - 不影响布局 - 性能开销小
优化: - 批量修改样式 - 使用 class 代替 style - 使用 transform 代替 top/left - 避免强制同步布局
5.3 如何优化渲染性能?
策略:
1. 减少重排重绘 - 批量修改 DOM - 使用 transform - 使用 DocumentFragment
2. 优化关键渲染路径 - 内联关键 CSS - 异步加载 JS - 压缩资源
3. 使用合成层 - transform - will-change - GPU 加速
4. 虚拟滚动 - 只渲染可见区域 - 减少 DOM 数量
5. 懒加载 - 图片懒加载 - 路由懒加载 - 组件懒加载
六、总结
初级境界: - 理解渲染流程 - 知道 DOM、CSSOM、Render 树 - 了解布局、绘制、合成
中级境界: - 理解重排和重绘 - 知道触发条件 - 会基本优化
高级境界: - 避免强制同步布局 - 使用 transform - 使用 requestAnimationFrame - 虚拟滚动
资深境界: - 理解渲染层合成 - 优化关键渲染路径 - 掌握性能指标 - 深入浏览器原理
面试要点: - 完整的渲染流程 - 重排和重绘的区别 - 性能优化策略 - 实际案例分析
掌握这些知识,你就能在面试中游刃有余,吊打面试官。
如果这篇文章对你有帮助,欢迎点赞收藏。