浏览器是怎么把代码变成页面的?

你在地址栏输入一个URL,敲下回车,页面就出现了。但浏览器内部到底经历了什么?HTML、CSS、JS是如何变成你看到的页面的?

今天用装修房子的故事,聊聊浏览器的渲染原理。


原文地址

墨渊书肆/浏览器是怎么把代码变成页面的?


从URL到页面:渲染总览

当你在浏览器输入URL并回车,浏览器内部经历了:

yaml 复制代码
浏览器地址栏
├── URL输入
├── DNS解析
│   └── 域名 → IP地址
├── TCP连接
│   └── 三次握手
├── HTTP响应
│   └── 服务器返回HTML/CSS/JS
└── 渲染进程处理
    ├── 构建阶段:HTML解析 + CSS解析
    └── 绘制阶段:布局 → 分层 → 绘制 → 合成

渲染流水线可以分为构建阶段绘制阶段

yaml 复制代码
构建阶段(并行):
┌─────────────┐     ┌─────────────┐
│  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 复制代码
<html>
  <head>
    <title>标题</title>
  </head>
  <body>
    <h1>欢迎</h1>
    <p>这是段落</p>
  </body>
</html>

DOM树结构:

yaml 复制代码
html
├── head
│   └── title → "标题"
└── body
    ├── h1 → "欢迎"
    └── p → "这是段落"

HTML解析过程

解析器从上到下读取HTML,遇到<head>标签创建head节点,遇到<body>标签创建body节点,遇到嵌套标签创建子节点...

yaml 复制代码
HTML解析器:逐行读取 → 创建节点 → 构建DOM树
<html> → html节点
<head> → head节点 → title节点 → 文本节点 → 关闭title → 关闭head
<body> → body节点 → h1节点 → 文本节点 → 关闭h1 → p节点 → 文本节点 → 关闭p → 关闭body → 关闭html
→ DOM树构建完成

遇到JS会怎样?

HTML解析器遇到<script>标签时会暂停解析,先执行JS:

yaml 复制代码
解析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样式表的树状结构,描述了每个元素的样式信息。

css 复制代码
body { font-size: 16px; }
h1 { color: red; font-size: 24px; }
p { color: blue; }

CSSOM树结构:

yaml 复制代码
body
├── font-size: 16px
├── color: (inherited)
└── children
    ├── h1
    │   ├── color: red
    │   └── font-size: 24px
    └── p
        └── color: blue

CSS解析特性

与HTML不同,CSS解析是上下文相关的

yaml 复制代码
标签选择器: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)

渲染树构建完成后,浏览器计算每个元素的几何信息:位置、大小、边距、边框等。

布局计算

yaml 复制代码
渲染树 → 布局计算 → 盒模型信息
元素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中的盒模型定义了元素的空间占用:

yaml 复制代码
┌─margin─────────────────────────────┐
│  ┌─border───────────────────────┐  │
│  │  ┌─padding──────────────────┐ │  │
│  │  │  ┌─content─────────────┐ │ │  │
│  │  │  │   width × height   │ │ │  │
│  │  │  └─────────────────────┘ │ │  │
│  │  └──────────────────────────┘ │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘
属性 说明
content 内容区域(width × height)
padding 内边距,内容与边框之间的空间
border 边框,围绕内边距的线条
margin 外边距,边框与其他元素之间的空间

回流(Reflow)

当元素的几何信息发生变化 时,浏览器需要重新计算布局,这称为回流(Reflow)

触发回流的操作:

  • 添加/删除可见DOM元素
  • 元素位置/尺寸变化
  • 浏览器窗口大小变化
  • 获取元素的offsetWidth/Height(强制触发计算)
yaml 复制代码
回流过程:
修改DOM → 重新计算布局 → 重绘(耗时操作)

回流比重绘更昂贵,因为它需要重新计算整棵布局树。


第五步:分层(Layer)

布局完成后,浏览器根据一定规则把页面分成多个图层(Layer)

为什么要分层?

分层可以让页面的不同部分独立绘制和合成,避免互相影响。

yaml 复制代码
分层示意:
Layer 3: 固定定位的导航栏(最顶层)
Layer 2: 主体内容
Layer 1: 背景图片
Layer 0: 页面根元素(最底层)

哪些元素会生成独立图层?

生成独立图层的触发条件:

  • position: fixed(固定定位)
  • will-change: transform(transform动画)
  • <video><canvas>元素
  • 3D变换:transform: translate3d()
  • CSS动画:@keyframes + transform
  • 加速属性:opacitytransform

浏览器会为这些元素创建独立的合成层(Compositing Layer),让它们的渲染不影响其他图层。

CSS Containment

contain属性可以告诉浏览器元素内容独立于页面其他部分,帮助浏览器优化:

css 复制代码
.container {
  contain: content;  /* 布局、样式、绘制都独立 */
}

第六步:绘制(Paint)

分层后,每个图层内部需要绘制,生成绘制指令。

绘制顺序

浏览器按从后到前的顺序绘制各图层:

yaml 复制代码
绘制顺序:
1. 背景色(最底层)
2. 背景图片
3. 边框
4. 内容(从左上到右下)
5. 伪元素
6. 轮廓(最顶层)

绘制指令

绘制不是直接画像素,而是生成绘制指令列表(Paint Records):

yaml 复制代码
绘制指令示例:
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)

当元素的外观改变但不影响布局时,触发重绘:

yaml 复制代码
触发重绘(不改布局):改变颜色、改变可见性、改变边框样式
改变样式 → 重绘 → 完成(比回流快)

重绘比回流快,因为它不需要重新计算布局。


第七步:合成(Composite)

绘制完成后,所有图层提交给GPU,GPU将各图层合成成最终画面。

合成过程

yaml 复制代码
Layer 0(背景层)
Layer 1(内容层)
Layer 2(浮动层)
    ↓
GPU合成 → 输出到屏幕

为什么需要合成层?

  1. 滚动流畅:合成层有自己的GPU加速,滚动不经过主线程
  2. 动画流畅:transform/opacity动画在合成线程执行,不被JS阻塞
  3. 分离更新:只有一个图层内容变化,只需重绘该图层
yaml 复制代码
传统渲染(无合成层)
└── JS修改 → 重排 → 重绘 → 合成 → 输出
    └── 主线程执行(可能被JS阻塞)

现代渲染(有合成层)
├── JS修改 → 重排 → 重绘 → 合成 → 输出
└── 合成线程独立执行(不受JS阻塞)

关键渲染路径(Critical Rendering Path)

关键渲染路径是浏览器从接收HTML到首次绘制页面的最短路径

优化关键渲染路径

想让页面更快显示?优化关键渲染路径:

优化目标 说明
减少关键资源数量 合并文件,减少请求
减少关键资源大小 压缩文件,删除注释空格
缩短关键路径长度 内联CSS、JS放底部、懒加载

回流与重绘:性能杀手

浏览器渲染过程中最怕什么?频繁的回流和重绘

强制回流/重绘

某些CSS属性和方法会强制触发回流或重绘:

javascript 复制代码
// 读取以下属性会强制触发回流
element.offsetWidth;     // 布局信息
element.offsetHeight;
element.scrollTop;
element.clientWidth;
getComputedStyle(element).width;

// 修改DOM结构
element.appendChild(child);
element.removeChild(child);

批量读写原则

读写分离,避免交叉触发回流:

javascript 复制代码
// 错误:每次读取触发一次回流
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

javascript 复制代码
// 不推荐:可能在帧之间执行
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(首次输入延迟)
相关推荐
程序员陆业聪2 小时前
微前端状态管理的真相:Module Federation + 跨应用通信实战
前端
flytam2 小时前
Claude Agent SDK 深度入门指南
前端·aigc·agent
weixin199701080162 小时前
《电天下商品详情页前端性能优化实战》
前端·性能优化
速易达网络2 小时前
vue+echarts开发的图书数字大屏系统
前端
小智社群2 小时前
贝壳获取小区的名称
开发语言·前端·javascript
Ferries3 小时前
《从前端到 Agent》系列|03:应用层-RAG(检索增强生成,Retrieval-Augmented Generation)
前端·人工智能·机器学习
Jessica_Lee3 小时前
Openclaw智能体终止机制
javascript
米丘3 小时前
Connect 深度解析:Node.js 中间件框架的基石
javascript·http·node.js
饺子不吃醋3 小时前
执行上下文:变量提升、作用域与 this 底层机制
javascript