你在地址栏输入一个URL,敲下回车,页面就出现了。但浏览器内部到底经历了什么?HTML、CSS、JS是如何变成你看到的页面的?
今天用**"装修房子"**的故事,聊聊浏览器的渲染原理。
原文地址
从URL到页面:渲染总览
当你在浏览器输入URL并回车,浏览器内部经历了:
浏览器地址栏
├── URL输入
├── DNS解析
│ └── 域名 → IP地址
├── TCP连接
│ └── 三次握手
├── HTTP响应
│ └── 服务器返回HTML/CSS/JS
└── 渲染进程处理
├── 构建阶段:HTML解析 + CSS解析
└── 绘制阶段:布局 → 分层 → 绘制 → 合成
渲染流水线可以分为构建阶段和绘制阶段:
构建阶段(并行):
┌─────────────┐ ┌─────────────┐
│ HTML解析 │ │ CSS解析 │
│ 生成DOM │ │ 生成CSSOM │
└──────┬──────┘ └──────┬──────┘
│ │
└────────┬───────────┘
↓
渲染树构建
↓
绘制阶段:
```yaml
绘制阶段
├── 布局计算
│ └── 计算每个元素的位置、大小、边距
├── 分层
│ └── 哪些元素需要独立图层(fixed/动画/视频)
├── 绘制
│ └── 生成绘制指令(矩形、文字、线条)
└── 合成输出
└── GPU合并图层 → 显示到屏幕
解读:
- 构建阶段:HTML和CSS解析同时进行(并行),完成后合并成渲染树
- 绘制阶段:按顺序执行布局、分层、绘制、合成,最终输出画面
阶段 | 输入 | 输出 |
|---|---|---|
| HTML解析 | HTML字符串 | DOM树 |
| CSS解析 | CSS字符串 | CSSOM树 |
| 渲染树构建 | DOM + CSSOM | 渲染树 |
| 布局 | 渲染树 | 盒模型信息 |
| 分层 | 布局信息 | 图层树 |
| 绘制 | 图层 | 绘制指令 |
| 合成 | 图层+指令 | 画面 |
第一步:HTML解析 → DOM树
浏览器收到HTML响应后,首先要解析HTML,构建DOM树。
DOM是什么?
DOM(Document Object Model,文档对象模型)是HTML/XML文档的编程接口。浏览器把HTML文档解析成一棵树状结构,每个HTML标签都是树上的一个节点。
<html>
<head>
<title>标题</title>
</head>
<body>
<h1>欢迎</h1>
<p>这是段落</p>
</body>
</html>
DOM树结构:
html
├── head
│ └── title → "标题"
└── body
├── h1 → "欢迎"
└── p → "这是段落"
HTML解析过程
解析器从上到下读取HTML,遇到<head>标签创建head节点,遇到<body>标签创建body节点,遇到嵌套标签创建子节点...
HTML解析器:逐行读取 → 创建节点 → 构建DOM树
<html> → html节点
<head> → head节点 → title节点 → 文本节点 → 关闭title → 关闭head
<body> → body节点 → h1节点 → 文本节点 → 关闭h1 → p节点 → 文本节点 → 关闭p → 关闭body → 关闭html
→ DOM树构建完成
遇到JS会怎样?
HTML解析器遇到<script>标签时会暂停解析,先执行JS:
解析HTML → 构建DOM → 完成
↑
遇到<script>:暂停 → 执行JS → 继续
因为JS可能document.write()修改DOM,所以HTML解析器必须等JS执行完成才能继续。
这就是为什么把JS放在body底部可以加快首屏渲染——让HTML先解析完,显示内容,JS最后再执行。
第二步:CSS解析 → CSSOM树
HTML解析的同时,浏览器也在解析CSS,构建CSSOM树(CSS Object Model)。
CSSOM是什么?
CSSOM是CSS样式表的树状结构,描述了每个元素的样式信息。
body { font-size: 16px; }
h1 { color: red; font-size: 24px; }
p { color: blue; }
CSSOM树结构:
body
├── font-size: 16px
├── color: (inherited)
└── children
├── h1
│ ├── color: red
│ └── font-size: 24px
└── p
└── color: blue
CSS解析特性
与HTML不同,CSS解析是上下文相关的:
标签选择器:p { color: blue; } → 所有<p>生效
类选择器:.title { ... } → class="title"生效
ID选择器:#header { ... } → id="header"生效
CSS解析器需要考虑选择器优先级(ID > 类 > 标签)、层叠规则、继承规则等。
第三步:渲染树(Render Tree)
DOM树 + CSSOM = 渲染树(Render Tree)。
渲染树只包含可见节点——display: none的元素不会出现在渲染树中。
DOM + CSSOM = 渲染树
| DOM节点 | CSSOM样式 | 渲染树 |
|---|---|---|
| display:none | ✗ 不显示 | |
| 容器样式 | body | |
| ├─h1 | color:red | h1(red) |
| ├─p | display:none | ✗ 不显示 |
| └─span | color:green | span(green) |
注意:<p style="display: none">不会生成渲染树节点,但<p style="visibility: hidden">会生成(只是不可见)。
第四步:布局(Layout)
渲染树构建完成后,浏览器计算每个元素的几何信息:位置、大小、边距、边框等。
布局计算
渲染树 → 布局计算 → 盒模型信息
元素1:x=0, y=0, width=200, height=50
元素2:x=0, y=50, width=200, height=30
元素3:x=0, y=80, width=100, height=80
→ 每个元素都有精确的位置和大小
盒模型(Box Model)
CSS中的盒模型定义了元素的空间占用:
┌─margin─────────────────────────────┐
│ ┌─border───────────────────────┐ │
│ │ ┌─padding──────────────────┐ │ │
│ │ │ ┌─content─────────────┐ │ │ │
│ │ │ │ width × height │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ └──────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
| 属性 | 说明 |
|---|---|
content | 内容区域(width × height) |
padding | 内边距,内容与边框之间的空间 |
border | 边框,围绕内边距的线条 |
margin | 外边距,边框与其他元素之间的空间 |
回流(Reflow)
当元素的几何信息发生变化时,浏览器需要重新计算布局,这称为回流(Reflow)。
触发回流的操作:
- 添加/删除可见DOM元素
- 元素位置/尺寸变化
- 浏览器窗口大小变化
- 获取元素的offsetWidth/Height(强制触发计算)
回流过程:
修改DOM → 重新计算布局 → 重绘(耗时操作)
回流比重绘更昂贵,因为它需要重新计算整棵布局树。
第五步:分层(Layer)
布局完成后,浏览器根据一定规则把页面分成多个图层(Layer)。
为什么要分层?
分层可以让页面的不同部分独立绘制和合成,避免互相影响。
分层示意:
Layer 3: 固定定位的导航栏(最顶层)
Layer 2: 主体内容
Layer 1: 背景图片
Layer 0: 页面根元素(最底层)
哪些元素会生成独立图层?
生成独立图层的触发条件:
position: fixed(固定定位)will-change: transform(transform动画)<video>、<canvas>元素- 3D变换:
transform: translate3d() - CSS动画:
@keyframes+transform - 加速属性:
opacity、transform
浏览器会为这些元素创建独立的合成层(Compositing Layer),让它们的渲染不影响其他图层。
CSS Containment
contain属性可以告诉浏览器元素内容独立于页面其他部分,帮助浏览器优化:
.container {
contain: content; /* 布局、样式、绘制都独立 */
}
第六步:绘制(Paint)
分层后,每个图层内部需要绘制,生成绘制指令。
绘制顺序
浏览器按从后到前的顺序绘制各图层:
绘制顺序:
1. 背景色(最底层)
2. 背景图片
3. 边框
4. 内容(从左上到右下)
5. 伪元素
6. 轮廓(最顶层)
绘制指令
绘制不是直接画像素,而是生成绘制指令列表(Paint Records):
绘制指令示例:
1. drawRect(x=0, y=0, w=100, h=50) ← 矩形
2. drawText("Hello", x=10, y=30) ← 文字
3. drawRect(x=0, y=50, w=200, h=1) ← 分割线
这些指令会交给**光栅线程(Raster)**执行,将指令转换为实际像素。
重绘(Repaint)
当元素的外观改变但不影响布局时,触发重绘:
触发重绘(不改布局):改变颜色、改变可见性、改变边框样式
改变样式 → 重绘 → 完成(比回流快)
重绘比回流快,因为它不需要重新计算布局。
第七步:合成(Composite)
绘制完成后,所有图层提交给GPU,GPU将各图层合成成最终画面。
合成过程
Layer 0(背景层)
Layer 1(内容层)
Layer 2(浮动层)
↓
GPU合成 → 输出到屏幕
为什么需要合成层?
- 滚动流畅:合成层有自己的GPU加速,滚动不经过主线程
- 动画流畅:transform/opacity动画在合成线程执行,不被JS阻塞
- 分离更新:只有一个图层内容变化,只需重绘该图层
传统渲染(无合成层)
└── JS修改 → 重排 → 重绘 → 合成 → 输出
└── 主线程执行(可能被JS阻塞)
现代渲染(有合成层)
├── JS修改 → 重排 → 重绘 → 合成 → 输出
└── 合成线程独立执行(不受JS阻塞)
关键渲染路径(Critical Rendering Path)
关键渲染路径是浏览器从接收HTML到首次绘制页面的最短路径。
优化关键渲染路径
想让页面更快显示?优化关键渲染路径:
| 优化目标 | 说明 |
|---|---|
| 减少关键资源数量 | 合并文件,减少请求 |
| 减少关键资源大小 | 压缩文件,删除注释空格 |
| 缩短关键路径长度 | 内联CSS、JS放底部、懒加载 |
回流与重绘:性能杀手
浏览器渲染过程中最怕什么?频繁的回流和重绘。
强制回流/重绘
某些CSS属性和方法会强制触发回流或重绘:
// 读取以下属性会强制触发回流
element.offsetWidth; // 布局信息
element.offsetHeight;
element.scrollTop;
element.clientWidth;
getComputedStyle(element).width;
// 修改DOM结构
element.appendChild(child);
element.removeChild(child);
批量读写原则
读写分离,避免交叉触发回流:
// 错误:每次读取触发一次回流
element.width = element.offsetWidth * 2;
element.height = element.offsetHeight * 2;
element.marginTop = element.offsetTop * 2;
// 正确:先读后写,写只触发一次回流
const width = element.offsetWidth;
const height = element.offsetHeight;
const marginTop = element.offsetTop;
element.style.width = width * 2;
element.style.height = height * 2;
element.style.marginTop = marginTop * 2;
requestAnimationFrame
对于需要连续动画的场景,使用requestAnimationFrame代替setTimeout/setInterval:
// 不推荐:可能在帧之间执行
setTimeout(() => {
element.style.transform = 'translateX(100px)';
}, 16);
// 推荐:在下一帧开始前执行
requestAnimationFrame(() => {
element.style.transform = 'translateX(100px)';
});
总结:渲染流水线
阶段 | 输入 | 输出 | 耗时 |
|---|---|---|---|
| HTML解析 | HTML字符串 | DOM树 | 中 |
| CSS解析 | CSS字符串 | CSSOM树 | 中 |
| 渲染树构建 | DOM + CSSOM | 渲染树 | 快 |
| 布局 | 渲染树 | 盒模型信息 | 慢 |
| 分层 | 布局信息 | 图层树 | 中 |
| 绘制 | 图层 | 绘制指令 | 中 |
| 合成 | 图层+指令 | 画面 | 快 |
核心思想:浏览器渲染页面如同装修房子——先搭骨架(DOM),再刷墙(CSS),然后布局家具位置(Layout),最后上色绘制(Paint),不同房间(Layer)可以同时施工,最后统一验收(Composite)。
理解渲染原理,才能写出性能更好的页面。
扩展阅读
| 概念 | 说明 |
|---|---|
| 虚拟DOM | React等框架用JS对象模拟DOM,减少真实DOM操作 |
| 增量更新 | 只更新变化的部分,不全量重渲染 |
| Content-visibility | CSS新属性,跳过屏幕外内容的渲染 |
| 渲染性能指标 | LCP(最大内容绘制)、CLS(布局偏移)、FID(首次输入延迟) |