关键渲染路径
浏览器从接收 HTML 到页面内容渲染在屏幕上,需要经过一系列步骤。理解关键渲染路径(Critical Rendering Path, CRP)是 Web 性能优化的核心。本节将详细介绍从 HTML 到 DOM 树、CSS 到 CSSOM、再到 Render Tree、Layout(布局)、Paint(绘制)和 Composite(合成)的完整流程,以及如何优化每个阶段。
前置知识
阅读本节前,建议先了解:canonical URL
渲染流程概览
HTML → DOM Tree
+
CSS → CSSOM Tree
↓
Render Tree → Layout → Paint → Composite → 屏幕显示| 阶段 | 说明 | 耗时因素 |
|---|---|---|
| DOM 构建 | 解析 HTML 生成 DOM 树 | HTML 大小、复杂度 |
| CSSOM 构建 | 解析 CSS 生成 CSSOM 树 | CSS 大小、复杂度 |
| Render Tree | 合并 DOM 和 CSSOM,生成渲染树 | DOM + CSSOM |
| Layout | 计算元素位置和大小 | DOM 节点数、布局复杂度 |
| Paint | 将像素绘制到屏幕 | 元素数量、样式复杂度 |
| Composite | 合成各图层 | 图层数量、合成操作 |
DOM 树构建
浏览器解析 HTML 字节流,逐步构建 DOM 树:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CRP 示例</title>
<style>
body { font-family: sans-serif; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>标题</h1>
<p>段落内容</p>
<img src="photo.jpg" alt="照片" />
</body>
</html>DOM 树结构:
document
├── doctype: html
└── html
├── head
│ ├── meta
│ ├── title
│ └── style
└── body
├── h1
├── p
└── img增量解析
浏览器不会等所有 HTML 下载完才开始解析,而是增量式地边下载边解析。这就是为什么 <script> 默认会阻塞 HTML 解析——浏览器需要先下载并执行脚本。
CSSOM 构建
CSSOM(CSS Object Model)的构建是渲染阻塞的:
css
/* CSS 解析为 CSSOM 树 */
body {
font-family: sans-serif; /* body 规则 */
margin: 0; /* body 规则 */
}
h1 {
color: #333; /* h1 规则 */
font-size: 2rem; /* h1 规则 */
}
p {
line-height: 1.6; /* p 规则 */
}CSSOM 树结构:
body
├── font-family: sans-serif
└── margin: 0
h1
├── color: #333
└── font-size: 2rem
p
└── line-height: 1.6CSS 是渲染阻塞的
浏览器在构建 CSSOM 之前不会渲染任何内容。确保关键 CSS 尽快加载。
Render Tree(渲染树)
Render Tree 是 DOM 和 CSSOM 的合并结果,只包含需要显示的节点:
| DOM 节点 | CSSOM 匹配 | Render Tree 中? |
|---|---|---|
<head> | - | 否(不显示) |
<body> | body 规则 | 是 |
<h1> | h1 规则 | 是 |
<p> | p 规则 | 是 |
<meta> | - | 否 |
<script> | - | 否 |
html
<!-- 不会出现在 Render Tree 中的元素 -->
<head>...</head> <!-- 不显示 -->
<meta ... /> <!-- 不显示 -->
<script ...></script> <!-- 不显示 -->
<style ...></style> <!-- 不显示 -->
<!-- display:none 的元素也不会出现在 Render Tree 中 -->
<div style="display: none">隐藏内容</div>
<!-- visibility:hidden 的元素会出现在 Render Tree 中(占据空间) -->
<div style="visibility: hidden">不可见但占据空间</div>Layout(布局/重排)
Layout 阶段计算每个可见元素的确切位置和大小:
Layout 计算:
- 元素的位置(x, y 坐标)
- 元素的尺寸(width, height)
- 边距、内边距、边框
- 盒模型计算触发 Layout 的操作
javascript
// 以下操作会触发 Layout
element.offsetWidth
element.offsetHeight
element.getBoundingClientRect()
element.scrollLeft
window.getComputedStyle()
// 修改以下属性会触发 Layout(重排)
element.style.width = '100px'
element.style.height = '200px'
element.style.marginTop = '10px'
element.style.paddingLeft = '5px'
document.body.appendChild(newElement)布局范围
| 类型 | 说明 | 性能影响 |
|---|---|---|
| 全局 Layout | 整个页面的布局重新计算 | 高 |
| 局部 Layout | 仅影响特定元素及其子元素 | 低 |
Paint(绘制)
Paint 阶段将 Render Tree 中的元素转换为屏幕上的像素:
Paint 阶段:
- 文本渲染
- 颜色填充
- 边框绘制
- 图片绘制
- 阴影、渐变等视觉效果触发 Paint 的属性
javascript
// 以下属性修改会触发 Paint(重绘)
element.style.color = 'red'
element.style.backgroundColor = '#fff'
element.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)'
element.style.textShadow = '1px 1px 2px rgba(0,0,0,0.2)'Composite(合成)
现代浏览器使用分层合成来优化渲染:
Composite 阶段:
- 将各层按正确顺序合成
- 应用变换(transform、opacity)
- 最终输出到屏幕提升为独立图层
css
/* 以下属性会将元素提升为独立图层(减少重排重绘) */
.promoted-layer {
transform: translateZ(0); /* 3D 变换 */
will-change: transform; /* 声明将要变化的属性 */
opacity: 0.9; /* 透明度(部分浏览器) */
position: fixed; /* 固定定位 */
contain: layout; /* CSS Containment */
}渲染性能对比
| 操作 | 影响 | 性能 |
|---|---|---|
| 修改 transform/opacity | 仅 Composite | 最快 |
| 修改 color/shadow | Paint + Composite | 较快 |
| 修改 width/height | Layout + Paint + Composite | 最慢 |
实战示例:优化 CRP
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRP 优化示例</title>
<!-- 关键 CSS 内联 -->
<style>
/* 首屏需要的 CSS 直接内联 -->
body { margin: 0; font-family: sans-serif; }
.hero { height: 50vh; background: #1a1a2e; color: white; }
.hero h1 { font-size: 2rem; text-align: center; padding-top: 20vh; }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="styles/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles/non-critical.css"></noscript>
</head>
<body>
<div class="hero">
<h1>关键渲染路径优化</h1>
</div>
<main>
<!-- 首屏内容 -->
<p>这是首屏内容,无需等待非关键 CSS 和 JS 加载。</p>
</main>
<!-- 脚本放在底部,使用 defer 异步加载 -->
<script defer src="scripts/app.js"></script>
</body>
</html>注意事项
- CSS 是渲染阻塞的:确保关键 CSS 尽快加载
- JavaScript 默认阻塞解析:使用
defer或async异步加载 - 避免频繁的重排重绘:批量修改 DOM,使用
requestAnimationFrame - 合理使用图层:过多的独立图层会增加内存消耗
最佳实践
- 内联关键 CSS,延迟加载非关键 CSS
- 使用
defer或async加载 JavaScript - 使用
transform和opacity做动画,避免 Layout - 使用
will-change提前声明将要变化的属性 - 使用 CSS Containment 限制重排范围
- 使用 Chrome DevTools Performance 面板分析渲染瓶颈
下一节
继续学习:资源加载优先级