浏览器工作原理:从导航、获取资源到解析与渲染
Category(分类): Browser Status: 已更新
浏览器工作原理可以用一句话概括:用户发起导航后,浏览器解析地址、建立或复用网络连接,获取 HTML 及其依赖资源,再把文本解析成对象模型,经过样式计算、布局、绘制、栅格化和合成,最终把像素交给屏幕显示。
这个模型适合学习和定位问题,但不是所有页面都严格依次执行所有步骤。缓存、Service Worker、预连接、HTTP/2 多路复用、HTTP/3、BFCache、已有渲染进程以及增量更新,都可能改变实际路径。
一、导航:用户请求一个页面
导航可能由以下行为触发:
- 在地址栏输入 URL;
- 点击链接、提交表单或调用
location; - 前进/后退、刷新、打开新窗口;
- Service Worker、浏览器扩展或应用代码参与的导航。
在真正访问网络前,浏览器还可能命中 HTTP 缓存、Service Worker、BFCache,或者复用已有连接。命中 BFCache 时,页面甚至可以从历史快照恢复,不必重新下载和解析全部资源。
1. 解析 URL 和查找地址
以 https://example.com/articles?id=42#intro 为例:
| 部分 | 示例 | 作用 |
|---|---|---|
| scheme | https | 指定访问协议 |
| authority | example.com | 主机名,可能还包含端口和用户信息 |
| path | /articles | 资源路径 |
| query | ?id=42 | 传给服务器的查询参数 |
| fragment | #intro | 页面内片段,通常不会发送给服务器 |
浏览器会先判断输入内容是 URL 还是搜索关键词,并规范化 URL。片段标识符通常只在浏览器端用于定位文档中的元素,不会出现在 HTTP 请求目标中。
2. DNS 查询
主机名需要解析为一个或多个 IP 地址,但“第一次访问才会 DNS 查询”是过时的简化说法。浏览器、操作系统、路由器、递归 DNS 解析器和权威 DNS 都可能缓存结果;缓存过期、网络切换、连接迁移或记录变化时仍然可能重新查询。

典型链路可以概括为:
- 浏览器检查自身缓存和已有连接;
- 操作系统检查本地缓存,必要时向配置的递归解析器请求;
- 递归解析器根据 NS、A/AAAA、CNAME 等记录向权威服务器查询;
- 返回 IPv4 和/或 IPv6 地址,并根据 TTL 缓存;
- 浏览器根据地址、端口、代理和网络策略选择连接。

DNS 根服务器不是“600 多台固定服务器”。通常所说的根服务器是 13 个根服务标识,背后通过 Anycast 在全球部署了大量实例。开发者通常不需要直接访问根服务器,应用也不应假设某个域名只有一个固定 IP。
DNS 查询还可能通过 DoH 或 DoT 发送给解析服务。它们可以减少本地网络对 DNS 明文的观察,但不能替代 HTTPS,也不能隐藏后续连接的全部元数据。
3. 建立或复用连接
如果已有适合的连接,浏览器会尽量复用,而不是每个资源都重新握手。连接是否能复用取决于协议版本、主机、端口、代理、证书、连接状态和浏览器调度策略。
HTTP/1.1 和 HTTP/2:TCP
在 HTTP/1.1 或 HTTP/2 over TCP 的场景下,通常先进行 TCP 三次握手:
- 客户端发送
SYN,声明初始序列号和能力; - 服务端回复
SYN-ACK; - 客户端回复
ACK,连接进入可传输状态。

TCP 提供可靠的有序字节流,但它不理解 HTTP,也不负责加密。数据包丢失时,TCP 会通过确认、重传、拥塞控制和流量控制等机制处理。连接关闭通常还会涉及 FIN/ACK 交互,不能简单理解为固定的“发送四个包就结束”。
“同一域名最多 6 个 TCP 连接”是旧浏览器和 HTTP/1.1 场景中的经验值,不能作为今天的通用规则。HTTP/2 可以在一个连接中多路复用多个请求,浏览器还会根据资源优先级和服务器能力调整连接数量。
HTTP/3:QUIC
HTTP/3 使用 QUIC,QUIC 基于 UDP,但自己提供可靠传输、流、多路复用、拥塞控制和 TLS 1.3 集成。因此 HTTP/3 不需要先建立 TCP 连接;网络路径改变时,QUIC 还可以利用连接 ID 尝试维持连接。是否使用 HTTP/3 由客户端和服务端通过 Alt-Svc、HTTPS 记录等机制协商,失败时可以回退到其他版本。
4. TLS 协商
HTTPS 不是一个替代 HTTP 的应用层协议,而是把 HTTP 放在 TLS 保护的连接中。TLS 提供机密性、完整性和对端身份认证,但不能让服务器“看不见”请求的所有元数据。

现代 TLS 1.3 的学习模型如下:
- 客户端发送支持的版本、密码套件、随机数和临时密钥交换参数(常见为 ECDHE key share);
- 服务端选择参数,发送自己的 key share、证书链和握手签名;
- 浏览器验证证书链、域名的 SAN、有效期、用途和签名,并确认根证书来自受信任的信任库;
- 双方通过临时密钥交换得到共享秘密,再用 HKDF 派生握手密钥和应用数据密钥;
- 握手消息使用认证加密保护,双方发送
Finished后开始传输应用数据。
不要把旧式 RSA 密钥交换当成现代 HTTPS 的统一流程,也不要说“服务器用私钥加密数据、浏览器用公钥解密”。数字签名用于证明握手消息确实由证书对应的私钥方签名;实际 HTTP 数据通常使用派生出的对称 AEAD 密钥加密。TLS 1.2 仍可能存在于兼容场景,但新系统应优先使用 TLS 1.3,并关闭已知不安全的旧协议和密码套件。
二、获取数据:HTTP 请求和响应
1. HTTP 请求
完成必要的连接准备后,浏览器请求 HTML。请求由请求方法、目标、协议版本、头字段和可选请求体组成:
GET /articles?id=42 HTTP/2
Host: example.com
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, br
Accept-Language: zh-CN,zh;q=0.9
User-Agent: <browser>
HTTP 方法不只有 GET 和 POST,还包括 PUT、PATCH、DELETE、HEAD、OPTIONS 等。GET 通常用于获取资源,POST 常用于提交数据,但是否携带请求体由方法语义和具体实现决定,不能把“请求体只有 POST 才存在”当成协议规则。

请求头可能包含缓存验证信息、Cookie、认证信息、来源、内容协商和追踪防护字段。打开开发者工具的 Network 面板,可以查看真实的请求方法、URL、请求头、响应头、Timing 和优先级。
2. HTTP 响应
响应包括状态码、头字段和可选的响应体:
HTTP/2 200
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
Content-Encoding: br
<!doctype html>
状态码并不是只有 200 才表示成功:
2xx表示请求已成功处理;3xx表示重定向或缓存复用,例如304 Not Modified;4xx表示请求或权限方面的问题;5xx表示服务端处理失败。

一个页面的 HTML 可能引用其他资源:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>我的页面</title>
<link rel="stylesheet" href="styles.css">
<script src="main.js" defer></script>
</head>
<body>
<h1>这是我的页面</h1>
<p><a href="/about">关于我们</a></p>
<img src="my-image.jpg" alt="示例图片">
</body>
</html>
浏览器收到 HTML 后,会继续发现并请求 CSS、脚本、图片、字体、模块和 iframe 等依赖资源;它们是否立即请求、是否阻塞渲染以及优先级如何,取决于元素类型、属性、缓存和调度器。

3. TTFB、拥塞控制和连接复用
TTFB(Time to First Byte)通常表示从一次请求的开始到收到响应第一个字节的时间。它可以拆成排队、DNS、连接、TLS、服务端处理和响应传输等部分。对于导航监控,浏览器提供 PerformanceNavigationTiming,不要把“第一个 HTML 数据包通常是 14KB”作为固定事实。
TCP 慢启动会根据拥塞窗口、往返时延、确认和丢包等情况逐渐调整发送速率。初始窗口、分段大小和实际首包大小会随协议、系统、网络和配置变化;客户端在收到数据后会发送 ACK,丢包也不等于“客户端停止发送 ACK”。HTTP/2、HTTP/3 还会在更高层进行流调度和多路复用。
三、HTML 解析:字节流到 DOM
浏览器通常边接收 HTML 边解析,而不是等整个文件下载完成后才开始工作。HTML 解析包含两个相互配合的阶段:标记化(tokenization)和建树(tree construction)。
1. 标记化和建树
WHATWG HTML 标准定义了 HTML tokenizer、插入模式、开放元素栈、活动格式化元素列表等规则。HTML 解析器是带状态的容错算法,不应简单当作普通 XML 解析器,也不能只用“正则表达式匹配标签”替代。

例如,浏览器会对缺少闭合标签、错误嵌套、表格结构和重复 form 等情况执行规范规定的纠错。最终 DOM 可能与原始字符串看起来不完全相同,因此调试时应查看 Elements 面板中的 DOM,而不是只看 View Source。
<!doctype html>
<html>
<head>
<title>HTML parsing</title>
</head>
<body>
<p>Hello <strong>browser</strong></p>
</body>
</html>

DOM 是文档的对象模型,包含元素、文本、注释和属性等节点。DOM 节点越多不必然意味着页面一定慢,但复杂的选择器、布局关系、样式变化和频繁更新会增加后续工作量。
2. 解析器、阻塞资源和预加载扫描器
经典的同步脚本可能暂停 HTML 解析:
<script src="app.js"></script>
脚本需要执行时,浏览器可能等待脚本下载;脚本还可能依赖已经发现的样式。更常见的现代写法是:
<script src="app.js" defer></script>
<script type="module" src="main.js"></script>
<script async src="analytics.js"></script>
defer脚本在文档解析完成后、DOMContentLoaded前按文档顺序执行;async脚本下载完成后尽快执行,多个脚本之间没有执行顺序保证;- 模块脚本默认延迟执行,并支持模块依赖图;
async、defer和模块脚本的行为还会受到脚本是否为经典脚本、是否动态插入等因素影响;document.write()、同步脚本和脚本执行期间的 DOM 操作可能改变解析流程。
样式表通常会阻塞首次渲染,但不等同于“HTML 解析器永远停在 CSS 上”。经典脚本可能等待已发现的样式表,字体一般不会阻塞 HTML tokenizer。preload、预连接和预加载扫描器可以提前发现资源,但它们不等同于完整的第二个 HTML 解析器。

四、解析 CSS:CSSOM 与级联
浏览器会把样式表解析成可供样式计算使用的内部结构。document.styleSheets 提供 CSSOM 的一部分访问能力,但 CSSOM 不是一个与 DOM 完全对称、所有内部实现都公开的“规则树”。

样式计算会综合:
- 用户代理样式、作者样式和用户样式;
- 选择器匹配结果;
- 继承、层叠顺序、来源和
!important; - 媒体查询、容器查询、层叠层和自定义属性;
- 字体、视口、环境变量和动画状态。
CSS 选择器通常从右侧候选部分开始匹配是实现上的常见优化,但“CSS 规则从右到左阅读”不是 CSS 语言的语义,也不能据此武断判断所有选择器性能。
body {
color: #333;
font-size: 16px;
}
article {
color: tomato;
}
article .title {
margin-inline-start: 1rem;
}
article p {
color: #555;
}
<p><a href="/docs">文档链接</a></p>
a {
color: red;
}
p a {
color: blue;
}
第二条规则的选择器更具体,因此在没有其他来源和层叠条件干预时,链接会显示为蓝色。实际判断应使用现代 specificity 规则,而不是只比较选择器字符串长度。

五、JavaScript 引擎执行代码
浏览器渲染引擎负责 HTML、CSS、布局和绘制,JavaScript 引擎则负责 ECMAScript 代码的解析和执行。两者通过 DOM、Web IDL、事件循环等接口协作,但不是同一个“引擎”。
常见 JavaScript 引擎包括:
- V8:用于 Chromium 系浏览器,也用于 Node.js、Deno 等运行时;
- JavaScriptCore:WebKit 使用的 JavaScript 引擎;
- SpiderMonkey:Firefox 使用的 JavaScript 和 WebAssembly 引擎;
- Chakra:旧版 Microsoft Edge 使用过的引擎,现代 Edge 已迁移到 Chromium/V8。
现代引擎并不是简单的“逐行解释器”或“下载后立刻编译成机器码”。一个简化模型是:源码经过词法分析和语法分析生成 AST,之后可能生成字节码,由解释器执行;运行中收集类型和热点信息,再由基线编译器或优化编译器生成更高效的机器码。优化假设不成立时还可能反优化。
const age = 25
const message = `age: ${age}`
console.log(message)
JIT 是引擎实现策略,不是 ECMAScript 语法要求,也不能把“JavaScript 是解释型语言”或“JavaScript 是编译型语言”当成完整结论。WebAssembly 也不是“WebAssembley”,它是另一种具有明确格式和执行模型的 Web 代码形式。
六、可访问性树
除了 DOM、样式和布局相关结构,浏览器还会为辅助技术提供可访问性树或等价的可访问性表示。它通常包含角色(role)、可访问名称(name)、状态、属性、值和父子关系。

屏幕阅读器并不是直接读取 DOM 字符串,而是通过操作系统的辅助技术接口获取浏览器暴露的语义信息。可访问性树的生成和更新由浏览器实现决定,不应假设每次 DOM 更新都会同步、完整地重建一棵公开的树。
优先使用语义 HTML:
<button type="button" aria-expanded="false" aria-controls="menu">
打开菜单
</button>
<nav id="menu" hidden>
<a href="/docs">文档</a>
</nav>
只有在原生元素无法表达语义时再使用 ARIA。ARIA 可以补充角色和状态,但不能修复键盘行为、焦点管理、可见文本和实际交互缺失的问题。

七、从 DOM 和样式到像素
早期教程常用“DOM + CSSOM = Render Tree”描述渲染树。这个模型仍然有助于理解哪些节点参与显示,但现代浏览器内部通常还会维护布局树/fragment tree、属性树、绘制列表、合成层和图块等结构,不同引擎的名称和边界并不相同。
1. 样式计算和布局
布局阶段计算参与布局的盒子的尺寸和位置。display: none 的节点通常不产生布局盒;visibility: hidden 通常仍占据空间;伪元素、匿名盒、滚动容器和 iframe 可能产生额外的布局对象。

布局可能是全量,也可能只更新受影响的局部区域。窗口变化、字体加载、内容变化和几何属性变化都可能扩大影响范围,但不能从一个 CSS 属性名称准确推断实际成本。
2. 绘制、栅格化和合成
绘制阶段把背景、边框、文本、阴影和图片等转换为绘制指令或显示列表;栅格化再把需要显示的绘制内容转换为图块和位图;最后合成器根据层、变换、裁剪和透明度把结果合成到目标表面。


transform 和 opacity 在满足浏览器合成条件时可能只需要更新合成属性,从而跳过布局或重新绘制。但这不是所有 transform 都必然“只走 GPU”,filter、阴影、内容变化、内存压力、设备能力和浏览器启发式都可能让流程不同。

八、用开发者工具验证,而不是背固定军规
- Network 面板:查看 DNS、连接、TLS、请求优先级、缓存和 TTFB;
- Performance 面板:查看长任务、脚本、Recalculate Style、Layout、Paint、Raster 和 Composite;
- Elements 的 Computed/Accessibility 面板:查看计算样式、布局盒和可访问性信息;
- Rendering 面板:查看帧渲染统计、布局偏移、绘制边界等辅助信息;
- Lighthouse、CrUX 和 RUM:从实验室与真实用户两侧评估体验。
遇到性能问题时,应先用录制结果确认瓶颈,再决定是否改变 DOM 结构、样式、脚本、资源优先级或渲染策略。不要把“固定 6 个连接”“一定有 Render Tree”“所有动画都使用 GPU”当成跨浏览器、跨版本的规则。