- 引言 — 不到一秒钟里展开的一部史诗
- 第 1 步 — URL 解析与预处理
- 第 2 步 — DNS 解析:把名字变成地址
- 第 3 步 — TCP 三次握手:建立连接
- 第 4 步 — TLS 握手:搭建安全通道
- 第 5 步 — 发送 HTTP 请求
- 第 6 步 — 服务器的处理与响应
- 第 7 步 — 解析 HTML,构建 DOM
- 第 8 步 — 关键渲染路径:一路走到像素
- 再看一遍整体流程
- 结语
- 参考资料
引言 — 不到一秒钟里展开的一部史诗
"在地址栏输入 URL 并按下回车,会发生什么?"这是面试中的常客问题,但它常青是有原因的。这一行看似简单的动作里,压缩了 Web 这个系统几乎每一层:DNS、路由、传输层、加密、应用层协议,乃至渲染引擎。想把其中任何一环讲清楚,都得对全部环节略知一二。
这篇文章按顺序走一遍这趟旅程。从浏览器解释 URL 的那一刻,到屏幕上第一个像素落地的那一刻,我们会逐步追踪每个阶段里究竟发生了什么交互。示例地址就用 https://example.com/store/cart?id=42。
第 1 步 — URL 解析与预处理
按下回车的瞬间,浏览器首先要判断地址栏里的字符串到底是什么。这是一个 URL,还是一个搜索词?如果里面有空格,或者没有点(.),就会交给默认搜索引擎;如果看起来像 URL,就开始解析。
URL 可以拆成好几个部分。
https://example.com:443/store/cart?id=42#top
└─┬─┘ └────┬────┘└┬┘└───┬────┘└──┬──┘└┬┘
scheme host port path query fragment
- scheme(协议):
https。决定用哪种协议通信。 - host(主机):
example.com。要连接的服务器的域名。 - port(端口):省略时,
https默认为 443,http默认为 80。 - path(路径)、query(查询)、fragment(片段):分别表示服务器内想要哪个资源、要带上哪些参数、要滚动到文档内的哪个位置。
在这一步,浏览器已经会做一些优化。如果这个域名在 HSTS(HTTP Strict Transport Security)名单里,即便输入的是 http,也会被强制改写成 https。此外浏览器还会检查这个资源是否已经存在于浏览器缓存、Service Worker 或 HTTP 缓存中——如果有,甚至可以直接跳过整个网络请求。
第 2 步 — DNS 解析:把名字变成地址
主机名 example.com 是给人看的。网络之间的通信靠的是 IP 地址,所以需要把域名转换成 IP 地址,这就是 DNS(Domain Name System)解析。
浏览器会按顺序查看好几层缓存。只要某一层给出了答案,解析就立即结束——真正发出查询,只会发生在所有缓存都落空的时候。
- 先看浏览器自身的 DNS 缓存。
- 没有的话,看操作系统的解析器缓存(以及
hosts文件)。 - 还是没有,就去问配置好的递归解析器(recursive resolver)——通常是 ISP 提供的,或者是像 8.8.8.8 这样的公共 DNS。
如果连递归解析器也不知道答案,真正的递归查询就开始了。解析器会依次访问一连串权威服务器。
递归解析器
│ 1) "example.com 的 IP 是?"
▼
根服务器 -> ".com 归那边的 TLD 服务器管"
│
▼
.com TLD 服务器 -> "example.com 的名称服务器在这里"
│
▼
权威名称服务器 -> "example.com 是 93.184.216.34"
│
▼
解析器缓存结果并返回给浏览器
每个应答都带有一个 TTL(Time To Live)。在 TTL 有效期内,解析器会把这个答案留在缓存里,所以再次查询同一个域名时不必再跑到根服务器,可以很快得到答案。DNS 之所以大多数时候感觉很快,正是拜这层层缓存所赐。
出于性能考虑,浏览器还会在页面正式加载之前提前做 DNS 预取(DNS prefetch)。比如鼠标悬停在一个链接上的瞬间,浏览器可能就已经在后台悄悄开始解析了。
第 3 步 — TCP 三次握手:建立连接
已经拿到了 IP 地址,接下来要和那台服务器建立连接。因为 https 运行在可靠的传输层协议 TCP 之上,所以在发送任何数据之前,首先要建立 TCP 连接。这就是著名的三次握手(three-way handshake)。
客户端 服务器
│ ── SYN (seq=x) ─────────────►│ "我想连接"
│ │
│ ◄──── SYN-ACK (seq=y, ack=x+1)│ "好,我这边也准备好了"
│ │
│ ── ACK (ack=y+1) ───────────►│ "确认,开始吧"
▼ ▼
此后可以双向传输数据
三条消息中,两条交换完成之后,连接实质上就已经建立。每一步都会交换序列号(seq),这会成为之后排序数据、检测丢包的依据。这次握手至少要消耗 1 个 RTT(round-trip time,往返时间)。如果服务器在地球的另一端,光是这一次往返就可能要花上数百毫秒。
顺带一提,更新的 HTTP/3 协议用基于 UDP 的 QUIC 取代了 TCP,把建立连接和下一步的加密合并成一步,以此降低延迟。不过要理解概念,传统的 TCP + TLS 组合最为清晰,所以本文仍按这条路线来讲。
第 4 步 — TLS 握手:搭建安全通道
TCP 连接已经建立,但流经它的数据仍然是明文。https 中的 's' 指的正是 TLS(Transport Layer Security),在发送真正的 HTTP 数据之前,需要先搭建一条加密通道。TLS 握手主要做三件事:验证(authenticate) 服务器确实是它自称的那一个,交换(exchange) 用于 加密(encrypt) 通信的密钥,并 协商(negotiate) 要使用的加密方式。
以 TLS 1.3 为基准,把流程简化一下大致是这样。
客户端 服务器
│ ── ClientHello ────────────────►│ 支持的加密套件、密钥共享列表
│ │
│ ◄── ServerHello, Certificate ───│ 选定的加密方式、证书、密钥共享
│ + Finished │
│ │
│ ── Finished ───────────────────►│ 验证完成
▼ ▼
此后所有 HTTP 数据都以加密形式传输
把关键步骤拆开来看:
- 证书验证:服务器发来自己的证书。浏览器会检查这份证书是否由受信任的证书颁发机构(CA)签发、域名是否匹配、是否已过期。只要有一项对不上,就会跳出那个令人紧张的"连接不安全"警告。
- 密钥交换:双方使用公钥密码学(比如 ECDHE),生成一个即使窃听者看到全部往来消息也无法还原出来的共享密钥。用于加密实际数据的对称密钥,就是从这个共享密钥派生出来的。
- 加密套件协商:约定要使用哪一组加密与哈希算法的组合。
TLS 1.3 把整个过程压缩到了 1 个 RTT,对于之前访问过的服务器,甚至可以做到 0-RTT 恢复——相比旧版 TLS 1.2 需要 2 个 RTT,这是一个不小的进步。如果想亲自动手实验证书验证、加密套件、握手流程这些内容,可以在本站的认证与安全实验室里体验 TLS 及相关概念。
第 5 步 — 发送 HTTP 请求
加密通道搭建完成,终于轮到浏览器请求它真正想要的东西了。浏览器会构造并发送一条 HTTP 请求消息。以 HTTP/1.1 为例,请求大致是这样一段文本。
GET /store/cart?id=42 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, br
Cookie: session=abc123
Connection: keep-alive
逐行看一下各自的含义:
- 请求行:方法(
GET)、路径(/store/cart?id=42)、协议版本。 - Host 头:当一个 IP 上挂着多个站点(虚拟主机)时,用它告诉服务器你想要的是哪个域名。
- Accept 系列头:告诉服务器自己能接受哪些内容格式和压缩方式。
- Cookie 头:把服务器之前设置的 Cookie 带回去,用来维持登录状态之类的信息。
在 HTTP/2 和 HTTP/3 中,这些请求头不再是人类可读的文本,而是被压缩成二进制帧传输,而且一条连接可以同时携带多个请求(多路复用)。但它们所承载的信息在逻辑上和上面是一样的。
第 6 步 — 服务器的处理与响应
请求跨过网络到达服务器。不过"服务器"通常并不是单独一台机器,请求一般会经过好几层。
请求 --> [CDN / 边缘缓存] --> [负载均衡器] --> [反向代理]
│(命中缓存的话在这里直接响应)
--> [Web / 应用服务器] --> [数据库 / 缓存]
- CDN 与边缘缓存:静态资源(图片、CSS、JS)以及可缓存的页面,会由离用户更近的边缘服务器直接响应。这样就不必再跑到源站,速度快得多。
- 负载均衡器:把流量分摊到多个服务器实例上。
- 应用服务器:执行真正的业务逻辑。做路由、从数据库读取数据、渲染 HTML 或生成 JSON。
处理完成后,服务器返回 HTTP 响应。响应的第一行携带状态码。
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Encoding: br
Cache-Control: max-age=3600
Set-Cookie: session=abc123; HttpOnly; Secure
<!DOCTYPE html>
<html> ... </html>
状态码用三位数字概括结果。200 表示成功,301、302 表示重定向,404 表示资源不存在,500 表示服务器错误。如果好奇这些数字精确的含义与细微差别,可以在 HTTP 状态码图鉴里逐一查看。响应正文通常会用 gzip 或 br 压缩后再传输,所以浏览器要先把它解压出来。
第 7 步 — 解析 HTML,构建 DOM
浏览器开始接收 HTML 字节流时,并不会等到全部接收完才动手,而是边接收边解析。解析的目标,是把文档变成一棵树形结构——DOM(Document Object Model)。
HTML 字节流 --> 分词(tokenize) --> 生成节点 --> DOM 树
解析过程中,浏览器会遇到需要额外获取的资源:<link> 引用的 CSS、<script> 引用的 JavaScript、<img> 中的图片等等。这里有两个重要的性质。
- CSS 会阻塞渲染(render-blocking):在完全掌握样式信息之前,浏览器无法安全地把画面画出来。因此浏览器会把渲染推迟到获取并解析完 CSS、生成 CSSOM(CSS Object Model)为止。
- 脚本可能阻塞解析(parser-blocking):普通的
<script>一旦被遇到,就会立刻暂停 HTML 解析,下载并执行脚本——因为脚本有可能改变 DOM。为了避免这一点,可以给它加上async或defer属性,让它与解析并行,或者推迟到之后执行。
即便如此,浏览器仍会运行一个 预加载扫描器(preload scanner),在正式解析走到之前,提前开始下载接下来会用到的资源。多亏了它,即使脚本正在阻塞解析,其他资源的下载也能继续进行。
第 8 步 — 关键渲染路径:一路走到像素
DOM 和 CSSOM 都准备就绪后,浏览器会经过一系列步骤把二者结合起来、把画面画出来。整个过程称为关键渲染路径(Critical Rendering Path)。
DOM + CSSOM --> 渲染树 --> 布局 --> 绘制 --> 合成
各个阶段分别做这些事:
- 渲染树(Render Tree):把 DOM 和 CSSOM 结合起来,但只包含实际会显示在屏幕上的节点。
display: none的元素在这一步会被排除。 - 布局(Layout / Reflow):计算每个元素在屏幕上的位置和大小这类几何信息。视口尺寸发生变化时,这一步就得重新计算。
- 绘制(Paint):把每个元素的像素实际画出来。填充颜色、文字、图片、阴影等等。
- 合成(Composite):把被拆分到多个图层的绘制结果,按正确的顺序合并成最终画面。这一步由 GPU 加速。
这里与性能直接相关的两个概念是重排(reflow)和重绘(repaint)。用 JavaScript 改变元素的尺寸或位置,会触发从布局开始重新计算的重排;只改变颜色,则只会触发不涉及布局、直接重新绘制的重绘。重排比重绘代价更高,所以为了让动画流畅,更适合使用不会触发布局的属性(比如 transform、opacity)。这类属性只在合成阶段处理,可以跳过布局和绘制这两步。
再看一遍整体流程
把到目前为止的旅程汇总成一张图,大致是这样。
1. URL 解析 把输入字符串拆成 scheme/host/path
2. DNS 解析 域名 -> IP (多层缓存 + 递归查询)
3. TCP 握手 通过三次握手建立可靠连接 (1 RTT)
4. TLS 握手 认证 + 密钥交换 + 加密协商 (1 RTT)
5. HTTP 请求 携带方法/请求头/Cookie 发送
6. 服务器处理 经由 CDN/LB/应用服务器/数据库生成响应
7. HTML 解析 字节 -> DOM,CSS -> CSSOM
8. 渲染 渲染树 -> 布局 -> 绘制 -> 合成
每一步单拿出来都够写一本书,但从大的图景来看,它们最终都朝着同一个目标努力:把"人类可读的地址"变成"屏幕上的像素"。
结语
在地址栏输入 URL、按下回车的那短短一瞬间,名字解析、连接建立、身份认证、加密、数据传输、文档解析、画面渲染这些性质完全不同的工作,以精确的顺序严丝合缝地咬合运转。理解了这条流程之后,遇到性能问题时该怀疑哪一步、看到安全警告时到底哪里出了错,都能更快地找到方向。
下次觉得页面加载慢的时候,不妨想象一下,时间究竟是在这八个阶段中的哪一步流失的。是 DNS,是握手,是服务器处理,还是渲染?能够提出这个问题本身,就说明你已经把 Web 看得更深了一层。
参考资料
- MDN: How browsers work — https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work
- MDN: Populating the page — how browsers work — https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work
- Cloudflare Learning: What is DNS? — https://www.cloudflare.com/learning/dns/what-is-dns/
- Cloudflare Learning: What happens in a TLS handshake? — https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/
- web.dev: Critical rendering path — https://web.dev/articles/critical-rendering-path
- High Performance Browser Networking (Ilya Grigorik) — https://hpbn.co/
현재 단락 (1/121)
"在地址栏输入 URL 并按下回车,会发生什么?"这是面试中的常客问题,但它常青是有原因的。这一行看似简单的动作里,压缩了 Web 这个系统几乎每一层:DNS、路由、传输层、加密、应用层协议,乃至渲染...