浏览器渲染
从一个经典问题入手:从输入 URL 到按下回车,再到网页最终展示,浏览器发生了什么?
如果先忽略 DNS 查询、TCP 连接、TLS 握手等网络细节,整个过程可以粗略分为两个阶段:网络请求 和 页面渲染。
浏览器通过网络模块发起 HTTP/HTTPS 请求,与服务器建立通信,并获取返回的 HTML 内容。随后,浏览器会将这份 HTML
包装成一个渲染任务,放入渲染主线程的消息队列中。在事件循环机制的调度下,渲染主线程取出任务,并正式开启页面渲染流程。
我们来拆解:浏览器拿到 HTML 字符串后,如何把它渲染成最终页面。
整体流程
页面渲染大致可以分为以下几个阶段:
- 解析
HTML,构建DOM树 - 解析
CSS,构建CSSOM树 - 样式计算,得到每个节点的最终样式
- 布局,生成布局树
- 分层,生成图层树
- 生成绘制指令
- 分块与光栅化
- 合成与显示
1. 解析 HTML,构建 DOM 树
浏览器接收到的网络响应本质上是字节数据,也就是一串 0 和 1。虽然我们平时看到的是 HTML、CSS、JS
这样的文本文件,但在网络传输过程中,它们都会先以字节形式进行传递。
当浏览器接收到 HTML 字节数据后,会根据响应头、文档声明或默认规则确定编码方式,然后将字节数据解码为字符串。
得到字符串后,浏览器会进行标记化(tokenization):通过词法分析将字符串拆分成一个个标记(token),例如开始标签、结束标签、属性和文本内容等。
为什么需要标记化?
如果只是一整段字符串,浏览器无法直接理解其中的结构。通过标记化,浏览器才能知道哪些内容是标签、哪些内容是属性、哪些内容是文本节点。
比如下面这段 HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<span>
213
</span>
</div>
</body>
</html>
经过标记化和树构建阶段后,浏览器就能生成对应的 DOM 树。

2. 解析 CSS,构建 CSSOM 树
在解析 HTML 的过程中,如果遇到 <style> 标签,浏览器会解析其中的 CSS;如果遇到 <link rel="stylesheet">
,浏览器会请求对应的外部样式文件。
为了提升加载效率,浏览器通常还会通过预加载扫描器提前扫描后续的资源,例如样式表、脚本和图片,并尽早发起下载。
CSS 解析完成后,会形成一棵 CSSOM 树。CSSOM 记录了样式规则、选择器以及这些规则最终会如何作用到节点上。
需要注意的是:CSS 通常不会阻塞 DOM 树的构建,但会阻塞页面渲染。因为浏览器需要先知道节点的最终样式,才能继续进行后续的样式计算、布局和绘制。
3. JavaScript 对 HTML 解析的影响
除了 CSS,浏览器在解析 HTML 时也可能遇到外部 JS 文件或内联脚本。
如果主线程解析到普通的同步 <script> 标签,会暂停 HTML 解析,等待脚本下载并执行完成后,再继续构建 DOM 树。
因为
JS代码可能会通过document.write或 DOM API 修改当前文档结构,所以浏览器需要暂停HTML解析,保证脚本执行时看到的是当前准确的文档状态。
不过,并不是所有脚本都会以同样的方式阻塞解析。使用 async、defer 或 type="module" 时,脚本的下载和执行时机都会有不同。

4. 样式计算
有了 DOM 树之后,浏览器还不知道页面应该长什么样。因为 DOM 只描述了文档结构,而元素最终显示成什么样,还需要结合 CSS
规则来确定。
在样式计算阶段,主线程会遍历 DOM 树,并结合 CSSOM,为每个节点计算出最终样式,也就是 Computed Style。
这个过程中会发生几类转换:
- 没有显式设置的样式,会使用浏览器默认样式或继承值。
- 相对单位会被转换为更具体的计算值,例如
em会根据上下文计算出具体尺寸。 - 颜色关键字会被转换为具体颜色值,例如
red会变为rgb(255, 0, 0)。
为什么这里要说「大部分」和「很多预设」,可以参考:CSS 属性的计算过程。

5. 布局(Layout)
到这一步,浏览器已经知道了页面结构和每个节点的最终样式,但还不知道每个元素应该出现在页面的哪个位置、占据多大的空间。
布局阶段会根据 DOM 树和计算后的样式生成布局树(Layout Tree)。布局树中会包含页面中需要参与布局的节点,并记录它们的位置和尺寸信息。
每个布局节点通常会得到以下信息:
- 在页面中的位置坐标
- 盒子的宽高
- 边距、边框、内边距等盒模型信息
- 具体的边界盒信息(bounding box)
需要注意的是,display: none 的元素不会出现在布局树中,因为它不参与布局;而一些伪元素、匿名盒等不一定直接对应真实的 DOM
节点,但可能会参与布局。

6. 分层
完成布局后,浏览器还会根据页面情况进行分层,生成图层树(Layer Tree)。
分层的好处是:当某一层发生变化时,浏览器可以尽量只处理这一层,而不必重新处理整个页面,从而提升渲染效率。
例如,页面中的某些元素可能会因为以下原因被提升为单独图层:
- 使用了
transform或opacity动画 - 使用了
position: fixed - 使用了
will-change - 包含视频、Canvas 等特殊内容
- 浏览器根据自身策略进行图层优化
![]https://raw.githubusercontent.com/patty-yang/pic/images/browser-render_7.png
为了确定哪些元素应该放在哪一层,主线程会遍历布局树,并根据元素样式、层叠上下文和浏览器优化策略创建图层树。
7. 生成绘制指令
分层完成后,主线程会为每个图层生成绘制指令集。这些指令描述了这一层应该如何被画出来,但此时还没有真正把像素绘制到屏幕上。
可以把它理解为类似 Canvas 的绘制命令:
context.beginPath()
context.moveTo(10, 10)
context.lineTo(100, 100)
context.closePath()
这些绘制指令只是描述「怎么画」,真正的像素生成会发生在后面的光栅化阶段。
生成绘制指令后,渲染主线程会将每个图层的绘制信息提交给合成线程。
8. 分块与光栅化
页面可能很大,如果一次性把整个图层都绘制出来,成本会比较高。因此,合成线程会将每个图层划分为更小的区域,这个过程通常称为分块(tiling)。
分块之后,浏览器可以优先处理视口附近的图块,也可以将不同图块的处理任务分配给多个线程,从而提高效率。
光栅化(rasterization)就是把绘制指令转换为位图的过程。换句话说,它会把每个图块转成具体的像素信息。
光栅化通常由合成线程调度,并交给光栅化线程池处理;在开启硬件加速时,部分工作也可能由 GPU 参与完成。

9. 合成与显示
当图块完成光栅化后,合成线程会收集每个图层、每个图块的位置和变换信息,生成一个个绘制四边形(quad)。
quad 会描述位图应该画在屏幕的什么位置,同时也会记录旋转、缩放、透明度等合成信息。
随后,合成线程会把这些信息提交给 GPU,由 GPU 进行最终合成并显示到屏幕上。
这也是为什么 transform 和 opacity 的性能通常比较好:在很多情况下,它们可以只触发合成,而不需要重新执行布局和绘制。