浏览器渲染流程深度解析
从输入 URL 到像素上屏:DOM/CSSOM/渲染树/布局/绘制/合成,六阶段全拆解。
一句话概括
浏览器渲染管线分六步:HTML → DOM 树、CSS → CSSOM 树、两者合并为渲染树(Render Tree)、布局(Layout)计算位置大小、绘制(Paint)生成像素指令、合成(Composite)交给 GPU 上屏。理解每条管线,才知道从哪优化。
核心知识点
1. DOM 树构建:边下载边解析
浏览器不等 HTML 全部下载完就开始解析。解析器逐字节读入 → 分词(tokenize) → 构建 DOM 节点 → 插入树中。但遇到 <script> 会阻塞解析(因为 JS 可能 document.write),遇到 <link> 不阻塞 DOM 构建但阻塞渲染(等 CSS 才能画)。
1
2
3
4
5
6
7
8
9
<!-- 阻塞 DOM 构建 -->
<script src="heavy.js"></script>
<!-- 不阻塞 DOM 构建,但阻塞渲染 -->
<link rel="stylesheet" href="style.css">
<!-- 不阻塞任何东西(现代浏览器) -->
<script async src="analytics.js"></script>
<script defer src="app.js"></script>
2. CSSOM:为什么 CSS 要放 head
CSS 解析也是增量式的,但 CSSOM 不可逐步展示——一旦某个样式中途改变,之前渲染的部分就可能作废。所以浏览器必须等所有 CSS 加载完才会开始首次渲染。
这就是为什么 <link> 放 <body> 底部会导致 FOUC(闪白)——HTML 先渲染了默认样式,等 CSS 加载后又重新画。
3. 渲染树:只合并可见元素
DOM + CSSOM → Render Tree 时,display: none 的元素直接不进入渲染树(visibility: hidden 会进入,只是不可见占位不变)。伪元素 ::before/::after 也在此阶段加入渲染树。
1
2
3
4
5
/* 不会出现在渲染树中 */
.hidden { display: none; }
/* 在渲染树中,只是透明度为 0 */
.invisible { visibility: hidden; opacity: 0; }
4. 重排 vs 重绘 vs 合成
| 操作 | 触发 | 代价 | 示例 |
|---|---|---|---|
| 重排(Layout) | 改变几何属性 | 最高 | width/height/padding/margin/top/left |
| 重绘(Paint) | 改变外观不改变位置 | 中等 | color/background/box-shadow |
| 合成(Composite) | 只影响图层属性 | 最低 | transform/opacity(GPU 合成) |
1
2
3
4
5
6
7
8
9
10
11
12
// 经典批量修改减少重排
const el = document.getElementById('box');
el.style.display = 'none'; // 1 次重排
el.style.width = '200px'; // 在 display:none 下不触发重排
el.style.height = '100px';
el.style.display = 'block'; // 1 次重排
// 总计 2 次重排,而不是每行一次
// 更优方案:requestAnimationFrame 批量
requestAnimationFrame(() => {
el.style.cssText = 'width:200px; height:100px';
});
5. 分层与 GPU 合成
浏览器把页面切成多个图层(Composite Layers),独立光栅化后 GPU 合成。创建新图层的常见方式:
- 3D transform:
transform: translateZ(0)、translate3d(0,0,0) will-change: transform<video>、<canvas>、<iframe>
但图层不是越多越好——每个图层消耗 GPU 显存(Video RAM),低端设备上内存爆炸反而更卡。
其实你每天都在用
- CSS
will-change:提前告诉浏览器”这个元素会动”,浏览器预分配独立图层给 GPU @media print:打印时浏览器重建 CSSOM + 渲染树,隐藏导航栏只留内容- Lighthouse / PageSpeed Insights:FCP(首次内容绘制)、LCP(最大内容绘制)就是衡量渲染管线的关键指标
- Chrome Performance 面板:紫色=Paint,绿色=Composite——看到大片紫色说明绘制开销大
- 骨架屏/SSR:本质是预生成首屏 HTML 让浏览器一次性解析渲染,无需等 JS 再构建 DOM
常见误解(FAQ)
❌ 误区一:”把 DOM 操作放 requestAnimationFrame 就不触发重排”
rAF 只是把操作收集到同一帧执行,减少的是帧间的重复重排,而不是消除重排本身。真正的消除重排要靠 display:none、documentFragment、虚拟滚动等手段。
❌ 误区二:”transform 和 opacity 动画永远最快,因为它们只触发合成”
前提是该元素在独立合成层上。如果元素和兄弟元素在同一个层上,开启 transform 动画仍然可能触发整层的重绘。用 Chrome DevTools → Layers 面板确认元素是否在独立层。
❌ 误区三:”DOMContentLoaded 触发就说明渲染完成了”
DOMContentLoaded 只表示 DOM 解析完毕,此时 CSSOM 可能还没构建完,渲染树可能还在生成,像素远未上屏。渲染完成要看 Paint Timing API 的 FCP/LCP。
❌ 误区四:”减少 DOM 数量就能加速渲染”
DOM 数量是因素之一,更关键的是 CSS 选择器的复杂度和渲染树深度。一个深度嵌套的 div > div > div > div 比平铺的 100 个 div 重排成本更高,因为布局传播路径长。
一句话总结
浏览器渲染不是”拿到 HTML 一次性画出来”,而是一条分阶段的流水线——知道每一关在干什么、什么会阻塞哪一关,你才能写出快 100ms 的页面,而 100ms 可能就是转化率的分水岭。