浏览器渲染原理
在日常面试中,我们经常会遇到这样一个问题: “在浏览器输入 URL 后,页面是如何展现出来的?”
这个看似简单的问题,其实背后涉及浏览器渲染、网络请求、解析执行等一系列复杂流程。本文将聚焦于 浏览器渲染原理,带你梳完整过程(本文主要讲解渲染进程的工作,网络进程这里不详细解释)。
整个过程可以分为两个关键步骤:
- 浏览器解析 URL 并发起请求 —— 浏览器首先会解析你在地址栏输入的 URL,经过 DNS 查询、TCP/TLS 连接建立等步骤,将请求发送到服务器。
- 解析和渲染返回结果 —— 浏览器收到服务器返回的 HTML、CSS、JS 等资源后,会依次解析、构建 DOM、CSSOM等,最终通过渲染引擎将页面呈现出来。
浏览器解析 URL
第 1 步:处理输入
当用户在地址栏输入内容时,浏览器进程中的 界面线程 会立即启动,判断用户输入的是搜索关键词还是网址,并据此决定:要么将请求定向到搜索引擎,要么直接访问指定网站。
第 2 步:开始导航
用户按下 Enter 键后,UI 线程会发起网络请求以获取网站内容。此时,标签页角落会显示加载旋转图标,提示页面正在加载。
网络线程会按照网络协议处理请求,包括 DNS 查询 和建立 TLS 连接(如果是 HTTPS)。
若服务器返回 HTTP 301 等重定向响应,网络线程会通知 UI 线程,并根据重定向地址发起新的请求,完成导航流程。
第 3 步:读取响应
网络线程开始接收服务器返回的响应数据(载荷),并查看前几个字节以判断内容类型。虽然响应头中的 Content-Type 应指明数据类型,但由于可能缺失或错误,浏览器会执行 MIME 类型嗅探。
- 如果响应是 HTML,数据会传递给渲染进程进行渲染。
- 如果是 ZIP 或其他文件,则交由下载管理器处理。
同时,浏览器会进行安全检查:
- Safe Browsing:检查域名和内容是否为已知恶意网站。
- 跨源读取阻止 (CORB) :防止敏感跨站数据泄漏到渲染进程。
第 4 步:查找渲染进程
完成安全检查后,如果网络线程确认导航可继续,它会通知界面线程,界面线程随后查找或启动合适的 渲染进程,以准备渲染网页。
优化机制:由于网络请求可能需要数百毫秒,浏览器会在第 2 步发起请求时,就尝试并行启动渲染进程。这意味着当数据到达时,渲染进程通常已处于待机状态。
解析和渲染返回结果
当数据和渲染进程都准备就绪后,浏览器进程会向渲染进程发送一次 提交导航(Commit Navigation) 的 IPC 信号,并将来自服务器的 HTML 数据流交给渲染进程。
渲染进程在接收到该指令后,会将“解析 HTML”的工作加入自身 渲染主线程(Main Thread) 的消息队列。
在事件循环机制的调度下,渲染主线程从队列中取出该任务并开始执行,正式进入渲染流程。
整体的渲染步骤如下图所示:
![]()
第 1 步:解析 HTML 与构建 DOM / CSSOM
服务器返回的 HTML 本质上是 字节流,浏览器会将其解码为 字符串,再进一步解析成可操作的数据结构——这就是我们熟悉的 DOM 树。同样,CSS 也会被解析成对应的 CSSOM 树。
在解析过程中:
-
遇到 HTML 标签 ⇒ 主线程将其转换为 DOM 节点并加入 DOM 树中(树的根节点是
document)。 -
遇到 CSS ⇒ 根据层叠规则解析生成 CSSOM 树(根节点为
StyleSheetList)。
在浏览器解析 HTML 时,即使遇到
<link>或<style>标签,主线程也会继续向下解析 HTML,而不会被阻塞。这是因为浏览器会启动一个 预解析线程(Preload Scanner),提前发现并下载 CSS 等外部资源。 CSS 的下载和准备工作在这个预解析线程中完成,不会占用主线程,所以CSS 不会阻塞 HTML 解析。只有当 HTML 构建完成、CSS 下载完毕后,浏览器才会利用 CSS 生成 CSSOM 树,与 DOM 树结合形成 渲染树,用于后续页面绘制。 通过这种方式,浏览器实现了 DOM 构建与 CSS 下载的并行,提高了解析效率,同时保证页面渲染的正确性。
当浏览器解析到
<script>标签时,会暂停 DOM 的构建,等待外部脚本下载完成,并在主线程中完成解析与执行。
原因在于:JavaScript 有能力通过document.write、DOM API 等方式直接修改正在构建的 DOM 结构。
如果不暂停解析,一边构建 DOM、一边执行 JS,就可能导致 DOM 状态出现不一致。 因此,为了保证 DOM 构建过程的正确性和可预测性,浏览器必须中断 HTML 解析,优先执行脚本。这就是 JavaScript 会阻塞 DOM 构建的根本原因。
在解析结束之后,会得到:DOM 树 和CSSOM 树
第 2 步:样式计算
主线程会遍历构建完成的 DOM 树,并为树中每一个节点计算出它的最终样式,这个过程称为 样式计算(Computed Style) 。
样式计算结束后,我们获得的是一棵“附带最终样式信息的 DOM 树”,为后续布局阶段提供基础。
关于样式计算的具体细节,可以参考我另一篇文章:样式计算
第 3 步:布局
布局(Layout)是浏览器确定页面中每个元素几何位置和大小的过程。主线程会遍历 DOM 树,结合计算后的样式信息,生成包含 坐标、边界框尺寸 等几何信息的 布局树(Layout Tree) 。
布局树与 DOM 树的结构类似,但只包含 实际在页面中显示的内容:
- 应用
display: none的元素不会出现在布局树中,因为它们没有几何信息。 - 应用
visibility: hidden的元素仍然会在布局树中保留位置和尺寸信息。 - 伪元素(如
p::before { content: "Hi!"; })虽然不在 DOM 树中,但具有几何信息,因此会出现在布局树中。 - 其他情况如匿名块盒、匿名行盒等,也会导致布局树与 DOM 树不完全一一对应。
因此,大多数情况下,DOM 树和布局树并非严格对应,布局树只关注用于渲染的几何信息,而非完整的 DOM 结构。
第 4 步:分层
为了确定哪些元素需要位于哪些层,浏览器主线程会遍历 布局树 并生成 层树(Layer Tree) 。在 Chrome DevTools 的性能面板中,这一阶段通常显示为“更新层树”。
分层的核心目的是 提升渲染效率。浏览器会根据提示提前为元素分配独立层,优化动画或滚动等操作的渲染效率。
将页面划分为独立的层后,如果某个层的内容发生变化,浏览器只需要重绘该层,而无需重新渲染整个页面。
如下图所示:![]()
某些 CSS 属性和布局特性会自动触发分层,例如:
- transform、opacity、filter
- position: fixed / sticky
- overflow: scroll / auto
- z-index / 堆叠上下文
此外,如果希望浏览器为某些元素创建独立层,可以使用 will-change 属性向浏览器发出提示:
.menu {
will-change: transform;
}
补充说明
- 分层不会改变 DOM 结构或布局,只是为渲染过程划分优化单位。
- 虽然分层可以提高性能,但过度分层会增加 GPU 内存占用,因此应谨慎使用
will-change,仅对频繁变化的元素设置。
第 5 步:绘制(生成绘制指令)& 分块
-
绘制阶段(主线程)
- 主线程会为每个独立层生成对应的 绘制指令集(Paint Commands),用于描述这一层应该如何渲染,包括颜色、边框、文字、图片等。
- 绘制指令通常以矢量或命令序列的形式存在,而不是直接生成位图,这样可以提高渲染效率和重绘灵活性。
- 绘制完成后,主线程将每个层的绘制信息 提交给合成线程(Compositor Thread),主线程任务结束。
-
分块阶段(合成线程)
-
合成线程为了优化 GPU 渲染,会将每个图层划分为更小的 绘制块(Tiles) 。
-
分块的优势:
- 局部更新:只重绘变化的块,减少不必要的 GPU 负担。
- 并行处理:可从线程池中获取多个线程同时处理不同块,提高处理速度。
- 内存优化:分块渲染可以让 GPU 更好地管理显存,避免一次性加载大图层导致内存峰值过高。
-
![]()
第 6 步:光栅化
在渲染流程中,合成线程会将页面的图层信息(Layer / Tile)交给 GPU 进程,以高效完成 光栅化。
光栅化的本质是将矢量图形、文本、图层等内容转化为 像素位图(bitmap) ,方便最终在屏幕上显示。
- GPU 进程通常会开启多个线程并行处理不同的瓦片(Tile),提升渲染效率。
- 浏览器会优先处理靠近视口(Viewport)区域的块,以确保用户能尽快看到可视内容,提高 首屏渲染性能(FCP / First Contentful Paint) 。
- 光栅化完成后,每个图层或瓦片都会生成一块位图,为最终的 合成(Compositing) 做准备。
第 7 步:页面绘制
在光栅化完成后,合成线程(Compositor Thread) 会拿到每个图层(Layer)和瓦片(Tile)的位图,并生成对应的 绘制指引(quad) 。
- Quad 的作用:指示每块位图在屏幕上的显示位置,同时包含旋转、缩放、透明度等变换信息。
-
高效变换:变换(
transform)操作发生在合成线程,而不需要经过渲染主线程。这就是 CSStransform和opacity能够实现 GPU 加速、渲染性能高的原因。
合成线程生成 quad 后,会将其提交给 GPU 进程。GPU 进程通过系统调用与 GPU 硬件交互,完成最终的像素合成,将页面内容呈现在屏幕上。
补充总结:
- 渲染主线程负责构建 DOM、CSSOM、渲染树、布局和绘制图层内容;
- 合成线程只负责图层组合和变换操作;
- GPU 负责将最终像素输出到显示器,实现高效渲染。
流程总结:
第一步 浏览器解析 URL:
第二步:解析和渲染: