# 一个 HTTP 请求的一生：从输入 URL 到页面渲染

> 读者画像：你能熟练写业务代码、调 API，但一被问到「在浏览器地址栏输入 `www.example.com` 回车之后，到底发生了什么」，就只能背出零散的几个词——DNS、三次握手、渲染——中间的因果链却串不起来。这也是最经典的面试题之一。
>
> 读完这篇文章，你会得到一条**完整、按时间顺序、每一环都能量化耗时数量级**的心智地图。核心思路是：**一个 HTTP 请求的绝大部分时间不是花在「传数据」上，而是花在「等」上——等 DNS 查完、等握手来回、等阻塞的资源下载完。理解了「每一次网络往返（RTT）都是一次昂贵的等待」，这条链路的性能优化就几乎全都是「消灭往返」或「把往返藏起来」。**

全文的数字都标了数量级和量纲，尽量给出「典型值」而非「精确值」——真实数字取决于物理距离、网络质量和服务端负载，但**数量级的直觉**（是 1ms 还是 100ms 还是 1s）比精确值重要得多。文中涉及协议细节处标注了来源。

---

## 目录

1. [一张全景图与一个核心量纲：RTT](#1-一张全景图与一个核心量纲rtt)
2. [URL 解析与 DNS：把域名翻译成 IP](#2-url-解析与-dns把域名翻译成-ip)
3. [建立连接：TCP 三次握手 + TLS 握手](#3-建立连接tcp-三次握手--tls-握手)
4. [HTTP 请求与响应：请求行、状态码、缓存、Cookie](#4-http-请求与响应请求行状态码缓存cookie)
5. [协议演进：HTTP/1.1 → HTTP/2 → HTTP/3，队头阻塞的三次围剿](#5-协议演进http11--http2--http3队头阻塞的三次围剿)
6. [到达服务器之前：CDN、反向代理、负载均衡](#6-到达服务器之前cdn反向代理负载均衡)
7. [浏览器渲染：从字节流到屏幕像素](#7-浏览器渲染从字节流到屏幕像素)
8. [性能视角：每一环能怎么优化](#8-性能视角每一环能怎么优化)
9. [常见困惑澄清](#9-常见困惑澄清)
10. [一句话收尾](#10-一句话收尾)

---

## 1. 一张全景图与一个核心量纲：RTT

在钻进细节之前，先立一个全局框架。从你按下回车到页面「基本可看」，时间被切成这么几段：

```
输入 URL
  │
  ├─ ① DNS 解析         域名 → IP        （缓存命中 <1ms；冷查询 20~120ms）
  │
  ├─ ② 建立连接
  │      ├─ TCP 三次握手                （1 个 RTT）
  │      └─ TLS 握手（HTTPS）           （TLS 1.3 = 1 个 RTT；TLS 1.2 = 2 个 RTT）
  │
  ├─ ③ 发送 HTTP 请求 → 服务端处理 → 返回响应
  │      （首字节到达 = TTFB，含 1 个 RTT + 服务端处理时间）
  │
  ├─ ④ 下载 HTML，边下边解析
  │      └─ 遇到 CSS/JS/图片 → 回到 ②/③ 拉更多资源
  │
  └─ ⑤ 浏览器渲染：DOM → CSSOM → 渲染树 → 布局 → 绘制 → 合成 → 屏幕上有东西了
```

**贯穿全文的核心量纲是 RTT（Round-Trip Time，往返时延）**：一个数据包从客户端到服务端再回来的时间。它由物理距离决定，且有硬下限——光在光纤里的速度约 20 万 km/s，北京到上海直线约 1000km，光纤绕路后一个来回的物理极限就有约 10ms，加上路由设备转发，实际同城 RTT 常在 5~30ms，跨国 RTT 轻松到 100~300ms。

**为什么揪着 RTT 不放？** 因为握手、请求都是「发出去 → 等回来」的往返模式，每多一个往返，就多一个 RTT 的墙上时钟时间，而这段时间里 CPU 和带宽基本是**闲着的**。带宽可以花钱买，RTT 买不来（受光速限制）。所以你会看到：**这条链路上几乎所有严肃的优化，本质都是在减少 RTT 的个数，或者把 RTT 藏在用户察觉不到的地方。** 记住这句话，后面每一节都是它的注脚。

---

## 2. URL 解析与 DNS：把域名翻译成 IP

### 2.1 先拆 URL

浏览器拿到 `https://www.example.com:443/path?q=1#top`，第一步是解析这个字符串：

| 部分 | 值 | 作用 |
|---|---|---|
| scheme（协议） | `https` | 决定用 TLS、默认端口 443 |
| host（主机名） | `www.example.com` | 要做 DNS 解析的目标 |
| port（端口） | `443`（https 默认，可省略） | TCP 连接到哪个端口 |
| path（路径） | `/path` | 发给服务器的资源路径 |
| query（查询串） | `?q=1` | 附带参数 |
| fragment（锚点） | `#top` | **只在浏览器本地生效，不会发给服务器** |

一个容易忽略的点：`#top` 锚点是纯客户端的，浏览器用它来滚动定位，**永远不会出现在发往服务器的 HTTP 请求里**。这也是为什么单页应用早期用 `#` 做路由能避免请求服务器。

计算机之间只认 IP 不认域名，所以下一步必须把 `www.example.com` 翻译成一个 IP 地址（如 `93.184.216.34`）。这就是 DNS 干的事。

### 2.2 递归查询 vs 迭代查询：谁替谁跑腿

DNS 的完整解析涉及一整套层级服务器：**根服务器（`.`）→ 顶级域 TLD 服务器（`.com`）→ 权威服务器（`example.com` 的 NS）**。关键要区分两种查询方式：

- **递归查询（Recursive）**：你（客户端）对着**本地递归解析器（Local Resolver，通常是 ISP 或 8.8.8.8 这类公共 DNS）**说：「给我 `www.example.com` 的 IP，我不管你怎么查，查到了直接告诉我结果。」——你把责任**全权委托**出去了，只等一个最终答案。
- **迭代查询（Iterative）**：递归解析器替你去跑腿时用的方式。它先问根服务器，根说「我不知道，但 `.com` 的 TLD 服务器地址在这」；再问 TLD，TLD 说「我不知道，但 `example.com` 的权威服务器在这」；最后问权威服务器，才拿到真正的 IP。每一步都是**「不直接给答案，只给下一跳线索」**。

一句话记牢：**客户端 → 递归解析器是「递归」（我只要结果）；递归解析器 → 各级权威服务器是「迭代」（一跳一跳自己问）。** 面试里把这两个方向说反是高频失分点。

### 2.3 缓存层级：绝大多数查询根本走不到根服务器

如果每次访问网站都要跑一遍「根 → TLD → 权威」，DNS 早就崩了。真相是：**这套完整流程极少发生，因为每一层都有缓存**。一次解析请求会依次撞上这些缓存层，任一层命中就直接返回：

```
浏览器自身 DNS 缓存（Chrome 内部有几十条、几分钟）
      ↓ 未命中
操作系统 DNS 缓存（macOS 的 mDNSResponder / Windows 的 DNS Client 服务；本机 hosts 文件也在这一层）
      ↓ 未命中
路由器缓存（家用路由器常兼做本地 DNS 缓存）
      ↓ 未命中
ISP / 公共递归解析器缓存（命中率最高的一层，海量用户共享）
      ↓ 未命中（才真正开始迭代查询）
根服务器 → TLD 服务器 → 权威服务器
```

每一层缓存都受该记录的 **TTL（Time To Live）** 约束——权威服务器在返回记录时会附带一个 TTL（比如 300 秒），各级缓存最多缓存这么久。TTL 设短，切换 IP（比如故障转移）快，但缓存命中率低；设长则相反。这是运维配 DNS 时的经典权衡。

**耗时数量级**：

| 情况 | 典型耗时 |
|---|---|
| 浏览器/OS 缓存命中 | < 1ms（就是查个本地哈希表） |
| ISP 递归解析器缓存命中 | 一个到解析器的 RTT，约 1~20ms |
| 完全冷查询（走完根→TLD→权威） | 20~120ms，跨国或权威服务器远时更久 |

### 2.4 提前量：dns-prefetch 与 preconnect

既然 DNS 是链路开头的一段串行等待，一个自然的优化是：**在真正需要之前就把它做掉**。HTML 里可以放这两种提示：

```html
<!-- 只提前做 DNS 解析，成本极低，适合"可能会用到"的第三方域名 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- 一步到位：DNS + TCP + TLS 全部提前建好，适合"马上一定会用"的关键域名 -->
<link rel="preconnect" href="https://cdn.example.com">
```

区别在于「提前多少」：`dns-prefetch` 只提前做 DNS 解析（省 20~120ms），成本几乎为零，可以撒网式地对多个域名用；`preconnect` 把 DNS + TCP 握手 + TLS 握手**整条建连流程**都提前跑完（省的时间多得多，可达上百 ms），但每个 preconnect 都会真的占用一个连接和握手开销，所以只对「确定马上要用」的少数关键域名用（一般不超过 4~6 个），滥用反而拖慢首屏。

> 补充：**DoH（DNS over HTTPS）/ DoT（DNS over TLS）** 是近年 DNS 的重要演进——传统 DNS 查询是明文 UDP，任何中间人都能看到甚至篡改你在访问哪些域名。DoH 把 DNS 查询封装进 HTTPS，DoT 封装进 TLS，都为了隐私和防篡改。Chrome、Firefox 已默认或可选启用。代价是多了一层加密开销，且让运营商/企业更难做 DNS 层面的管控。

---

## 3. 建立连接：TCP 三次握手 + TLS 握手

拿到 IP，就要和服务器建立一条可靠的通信管道。对 HTTPS 网站，这分两步：先建 TCP 连接（可靠传输），再建 TLS 连接（加密）。

### 3.1 TCP 三次握手：确认「双方都能收发」

TCP 是可靠传输协议，通信前必须先「握手」，确认双方的收发能力都正常。经典的三次握手：

```
客户端                                服务端
  │  ── SYN (seq=x) ──────────────▶    │   "我想连你，我的初始序号是 x"
  │                                    │
  │  ◀──── SYN+ACK (seq=y, ack=x+1) ── │   "收到；我也想连你，我的序号是 y，
  │                                    │    确认你的 x 收到了"
  │  ── ACK (ack=y+1) ────────────▶    │   "收到你的 y 了，开始通信吧"
  │                                    │
  连接建立（客户端在发出第三个 ACK 后即可开始发数据）
```

**为什么必须是三次，不能两次或四次？**

- **两次不够**：只有「客户端 SYN → 服务端 SYN+ACK」的话，服务端无法确认「客户端到底收到了我的 SYN+ACK 没有」，也就无法确认**客户端的接收能力和服务端的发送能力**是好的。第三次 ACK 就是补上这个确认。经典的反例是：一个因为网络延迟而滞留的旧 SYN 报文，如果只有两次握手，服务端会误以为客户端要建连而白白建立一个连接。三次握手能让客户端拒绝这个失效的旧连接。
- **四次多余**：第二步里服务端把「确认你的 SYN（ACK）」和「我也要建连（SYN）」合并成一个包发了，所以不需要四次。

**耗时：三次握手中，客户端在收到第二个包（SYN+ACK）后就可以开始发数据了，所以建立 TCP 连接对首个请求的开销是「1 个 RTT」。** 记住这个 1-RTT。

### 3.2 TLS 握手：协商加密，1.2 与 1.3 的往返差异

HTTP 是明文的，HTTPS = HTTP + TLS。TCP 管道建好后，还要在上面再做一次 TLS 握手，目的是：**验证服务器身份（证书）+ 协商出一把双方都知道、别人不知道的对称加密密钥。** 这一步是「HTTPS 比 HTTP 慢一点」的主要来源。

**TLS 1.2（旧，2 个 RTT）**：完整握手要两个来回——第一个 RTT 交换 `ClientHello`/`ServerHello`、证书、密钥交换参数；第二个 RTT 完成密钥确认（`Finished`）。也就是说，光 TLS 握手就要等 2 个 RTT。

**TLS 1.3（新，1 个 RTT，2018 年 RFC 8446 标准化）**：这是重大改进。TLS 1.3 把握手压缩到 **1 个 RTT**——客户端在第一个 `ClientHello` 里就直接把密钥交换参数（key share）一起发出去，服务端一个来回就能把密钥定下来并开始加密通信。它还砍掉了 TLS 1.2 里一堆不安全或冗余的东西：淘汰了 RSA 密钥交换、静态 DH、旧的分组密码模式，密码套件从上百种精简到寥寥几种，既更快又更安全。

**0-RTT（TLS 1.3 的进阶，会话恢复场景）**：如果之前连过这台服务器，客户端可以用缓存的会话信息，在**第一个包里就带上应用数据**（early data），实现 0 个 RTT 的数据发送。代价是 0-RTT 数据有**重放攻击**风险（攻击者可以截获并重复发送这个包），所以协议规定只有幂等操作（如 GET）才适合用 0-RTT，服务端也要做重放防护。

**量化「HTTPS 慢一点」到底慢多少**：

| 场景 | 建连总往返 | 相比纯 HTTP 多出的开销 |
|---|---|---|
| HTTP（无 TLS） | TCP 1 RTT | — |
| HTTPS + TLS 1.2 | TCP 1 RTT + TLS 2 RTT = 3 RTT | +2 RTT |
| HTTPS + TLS 1.3 | TCP 1 RTT + TLS 1 RTT = 2 RTT | +1 RTT |
| HTTPS + TLS 1.3 会话恢复(0-RTT) | 趋近 TCP 1 RTT | 几乎为零 |

假设同城 RTT 20ms，TLS 1.2 首次建连比 HTTP 多约 40ms，TLS 1.3 多约 20ms；跨国 RTT 150ms 时，这就是多 300ms vs 150ms 的差距——这也是为什么升级到 TLS 1.3、复用连接、就近接入 CDN 对 HTTPS 站点如此重要。

> 顺带说：证书里非对称加密（RSA/ECDHE）只用在握手阶段协商密钥，一旦密钥协商好，后续的实际数据传输用的是**对称加密**（AES 等），因为对称加密快得多。「非对称交换密钥 + 对称传数据」是所有 TLS 的基本套路。

### 3.3 连接复用与 Keep-Alive：不要每个请求都重新握手

上面算下来，一次 HTTPS 建连要 2~3 个 RTT，如果每请求一个资源就重新建连、断连，代价高得离谱（一个网页动辄几十上百个资源）。

**HTTP Keep-Alive（持久连接）** 就是解法：一个 TCP 连接在完成一次请求-响应后**不立即关闭**，而是保持一段时间，让后续请求复用同一条连接，从而**只在第一个请求付握手的 RTT，后面的请求直接省掉建连开销**。HTTP/1.1 默认开启持久连接（`Connection: keep-alive`），这是相对 HTTP/1.0 的一大改进。

但 HTTP/1.1 的连接复用有个天花板：**同一条连接上，请求必须一个接一个串行发（一个响应没回来，下一个不能发），** 这就引出了下一节的主角——队头阻塞。为绕开它，浏览器对同一个域名会**并发建立多条连接**（Chrome 等主流浏览器通常上限 6 条），让 6 个请求能同时跑。这也是「域名分片」（把资源分散到多个子域名以突破 6 连接限制）这种老优化手段的由来——不过在 HTTP/2 之后它已经过时甚至有害了。

---

## 4. HTTP 请求与响应：请求行、状态码、缓存、Cookie

连接建好，终于可以说「人话」了——发一个 HTTP 请求。

### 4.1 请求与响应的结构

一个 HTTP 请求由三部分组成：

```http
GET /path?q=1 HTTP/1.1          ← 请求行：方法 + 路径 + 协议版本
Host: www.example.com           ← 请求头（多行 key: value）
User-Agent: Mozilla/5.0 ...
Accept: text/html
Cookie: session=abc123
                                ← 空行，分隔头和体
（请求体：GET 通常没有；POST/PUT 在这里放数据）
```

响应结构对称：

```http
HTTP/1.1 200 OK                 ← 状态行：协议版本 + 状态码 + 原因短语
Content-Type: text/html; charset=utf-8   ← 响应头
Content-Length: 1256
Cache-Control: max-age=3600
Set-Cookie: session=abc123; HttpOnly
                                ← 空行
<html>...</html>                ← 响应体：真正的内容
```

从请求发出到**响应第一个字节到达客户端**，这段时间叫 **TTFB（Time To First Byte）**。它 ≈ 1 个 RTT（请求过去、响应回来）+ 服务端处理时间（查数据库、渲染模板等）。TTFB 是衡量「后端 + 网络」快不快的关键指标，前端优化管不到它，得靠后端和 CDN。

### 4.2 常见状态码：分类即语义

状态码的**第一位数字**就定了大类，记住分类比背具体码更有用：

| 类别 | 含义 | 高频具体码 |
|---|---|---|
| **1xx** 信息 | 请求已收到，处理中 | `100 Continue`（可以继续发请求体）、`101 Switching Protocols`（如升级到 WebSocket） |
| **2xx** 成功 | 请求成功处理 | `200 OK`、`201 Created`（POST 新建成功）、`204 No Content`（成功但无返回体） |
| **3xx** 重定向 | 需要进一步动作，通常是跳转 | `301 Moved Permanently`（永久重定向，会被缓存）、`302 Found`（临时重定向）、`304 Not Modified`（协商缓存命中，见下文） |
| **4xx** 客户端错误 | 请求本身有问题 | `400 Bad Request`、`401 Unauthorized`（未认证）、`403 Forbidden`（已认证但无权限）、`404 Not Found`、`429 Too Many Requests`（限流） |
| **5xx** 服务端错误 | 服务器处理时出错 | `500 Internal Server Error`、`502 Bad Gateway`（网关/代理拿到上游坏响应）、`503 Service Unavailable`（过载/维护）、`504 Gateway Timeout`（上游超时） |

几个高频混淆点：`401`（你没登录/凭证无效）vs `403`（你登录了但没权限）；`502`（代理连上游但上游给了坏响应）vs `504`（代理连上游但上游超时没响应）；`301`（永久，浏览器会缓存跳转、对 SEO 传递权重）vs `302`（临时，不该被长期缓存）。

### 4.3 缓存：强缓存与协商缓存

缓存是 Web 性能的重头戏，核心是**避免重复下载没变的资源**。分两级：

**① 强缓存（本地直接用，连请求都不发）** —— 由响应头 `Cache-Control` 控制：

```http
Cache-Control: max-age=3600      ← 3600 秒内，浏览器直接用本地副本，不发任何请求
```

常见指令：

| 指令 | 含义 |
|---|---|
| `max-age=N` | 资源在 N 秒内视为新鲜，直接用缓存 |
| `no-cache` | **可以缓存，但每次用前必须找服务器验证**（走协商缓存，别被名字骗了） |
| `no-store` | 彻底不缓存，每次都完整重新下载（用于敏感数据） |
| `public` | 允许任何中间节点（如 CDN）缓存 |
| `private` | 只允许浏览器缓存，中间代理不能缓存（含用户私有数据时用） |
| `must-revalidate` | 缓存过期后，必须去服务器验证，不能用过期副本凑合 |

**② 协商缓存（发个请求问服务器"变了没"，没变就回 304）** —— 当强缓存过期（或设了 `no-cache`），浏览器带着「缓存标识」去问服务器，服务器判断资源没变就回一个 **`304 Not Modified`（不带响应体）**，浏览器继续用本地副本。省的是**响应体的下载**（往返还是有的，但不用重传 body）。两套机制：

- **`Last-Modified` / `If-Modified-Since`（基于时间）**：服务器首次返回 `Last-Modified: <时间>`；下次请求浏览器带 `If-Modified-Since: <那个时间>`，服务器比对文件修改时间，没变就 304。缺点：精度只到**秒**（1 秒内多次修改识别不出）；且文件内容没变、仅修改时间变了（比如重新部署）会误判为「变了」；分布式多机部署时各机器文件时间还可能不一致。
- **`ETag` / `If-None-Match`（基于内容指纹）**：服务器首次返回 `ETag: "<内容哈希>"`；下次请求浏览器带 `If-None-Match: "<那个哈希>"`，服务器比对指纹，一致就 304。**ETag 直接反映内容本身**，绕开了 `Last-Modified` 的秒级精度和时钟不一致问题，更精确，是现代实践的首选（两者可同时存在，ETag 优先级更高）。

一图看懂缓存决策流：

```
请求资源
  │
  ├─ 强缓存(Cache-Control/max-age)未过期？── 是 ──▶ 直接用本地副本（连请求都不发，最快）
  │                                    否 │
  │                                       ▼
  ├─ 带 ETag/Last-Modified 去问服务器（协商缓存）
  │        │
  │        ├─ 服务器：没变 → 304 Not Modified ──▶ 用本地副本（省了 body 下载）
  │        └─ 服务器：变了 → 200 + 新内容      ──▶ 下载新副本并更新缓存
```

### 4.4 Cookie：给无状态的 HTTP 补上"记忆"

HTTP 本身是**无状态**的——服务器默认不记得你上个请求是谁。Cookie 就是补丁：服务器通过响应头 `Set-Cookie` 塞一小段数据给浏览器，浏览器存下来，之后每次请求同一站点都自动带上 `Cookie` 请求头，服务器借此识别用户（如登录态）。关键属性：`HttpOnly`（JS 读不到，防 XSS 窃取）、`Secure`（只在 HTTPS 下发送）、`SameSite`（限制跨站携带，防 CSRF）。注意 Cookie 会**随每个请求上行**，塞太多会拖慢每个请求，敏感/大数据应放别处（如 localStorage、服务端 session）。

---

## 5. 协议演进：HTTP/1.1 → HTTP/2 → HTTP/3，队头阻塞的三次围剿

HTTP 协议这些年的演进，可以用一条主线串起来：**不断地消灭「队头阻塞（Head-of-Line Blocking）」**——排在队伍最前面的那个慢家伙，卡住了后面所有人。这个问题在三个层次上出现，也被三代协议依次解决。

### 5.1 HTTP/1.1 的痛：应用层队头阻塞

HTTP/1.1 一条 TCP 连接上，请求-响应必须**严格串行**：发出请求 A，必须等 A 的响应完全回来，才能发请求 B。（曾有个叫「管线化 Pipelining」的机制想让请求连续发，但响应仍须按序返回，且实现问题多，基本没被采用。）

结果：如果响应 A 是个很大的、很慢的资源，它就卡住了后面所有请求——这就是**应用层的队头阻塞**。浏览器的应对是「人海战术」：对同一域名**并发开 6 条 TCP 连接**（见 3.3），让 6 个请求并行。但一个页面几十个资源，6 条连接还是不够用，剩下的仍得排队，且每条连接都有独立的握手成本。

### 5.2 HTTP/2：二进制分帧 + 多路复用，干掉应用层队头阻塞

HTTP/2（2015 年 RFC 7540）的核心武器是**多路复用（Multiplexing）**：

- **二进制分帧**：不再用文本传输，而是把消息拆成一个个二进制「帧（frame）」，每个帧标注了自己属于哪个「流（stream）」。
- **多路复用**：**一条 TCP 连接上可以同时跑多个流**，各请求的帧交错着传输，到对端再按流 ID 重组。这样一条连接就能并发处理成百上千个请求，**应用层队头阻塞被彻底解决**，也不再需要开 6 条连接和域名分片了。
- **HPACK 头部压缩**：HTTP 请求头有大量重复内容（每个请求都带一样的 User-Agent、Cookie）。HPACK 用静态表 + 动态表 + 哈夫曼编码压缩头部，省下可观带宽。
- **Server Push（服务端推送）**：服务端可以在客户端还没请求时就主动推送资源。但实践证明它很难用对（容易推送客户端已缓存的东西，浪费带宽，实测采用率仅约 1.25% 且常导致性能倒退），**Chrome 已于 106 版本（2022 年 10 月）默认禁用并移除了 HTTP/2 Server Push**，业界转向用 `<link rel="preload">` 和 **`103 Early Hints`** 等更可控的方式。这是一个「看起来很美但被淘汰」的特性——写「现状」时别再说它在广泛使用。

**但 HTTP/2 没解决更底层的一个问题**：它所有的流都跑在**同一条 TCP 连接**上。而 TCP 为了保证「可靠、按序」，一旦某个 TCP 数据包丢了，TCP 就必须停下来等它重传，**在它到达之前，后面所有已经到达的数据都不能交给上层**——哪怕那些数据属于别的、完全无关的流。这就是**TCP 层面的队头阻塞**：应用层不阻塞了，传输层又来阻塞。丢包率越高（弱网、移动网络），这个问题越致命，HTTP/2 在弱网下甚至可能比 HTTP/1.1 还慢。

### 5.3 HTTP/3（QUIC）：换掉 TCP，连传输层队头阻塞一起消灭

HTTP/3（2022 年 RFC 9114）的做法很激进：**抛弃 TCP，改用基于 UDP 的 QUIC 协议**。QUIC 在 UDP 之上自己重新实现了可靠传输、拥塞控制、加密。它带来三个关键好处：

1. **真正独立的流，消灭传输层队头阻塞**：QUIC 里每个流的丢包重传是**互相独立**的——流 A 的包丢了，只有流 A 停下来等重传，流 B、C 的数据照常向上层交付，互不影响。这就从根上解决了 HTTP/2 的 TCP 队头阻塞。
2. **建连更快，内置 TLS 1.3**：QUIC 把「传输层握手」和「TLS 1.3 加密握手」**合并**成一次，首次连接就是 **1-RTT**，会话恢复时甚至 **0-RTT**（对比 HTTPS over TCP 的 TCP 1-RTT + TLS 1-RTT = 2-RTT）。加密不再是可选项，而是协议内建。
3. **连接迁移（Connection Migration）**：传统 TCP 连接由「源IP+源端口+目的IP+目的端口」四元组标识，你从 WiFi 切到 4G，IP 变了，连接就断了、得重连。QUIC 用一个独立的 **Connection ID** 标识连接，与 IP 无关，所以**网络切换时连接不中断**——这对手机用户体验是巨大提升。

代价：QUIC 在用户态实现（不像 TCP 在内核），CPU 开销略高；且 UDP 在一些老旧网络设备上会被限速或拦截。但收益远大于代价，**目前 Google、Cloudflare、主流 CDN 和 Chrome/Firefox/Edge 都已广泛支持 HTTP/3**，它已经是新一代 Web 的默认方向。

**三代协议对比**：

| | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 底层传输 | TCP | TCP | **QUIC（基于 UDP）** |
| 应用层队头阻塞 | ❌ 有（串行请求） | ✅ 多路复用解决 | ✅ 解决 |
| 传输层队头阻塞 | 有（但每连接一请求，感知弱） | ❌ **仍有**（TCP 层丢包阻塞所有流） | ✅ **流独立，解决** |
| 首次建连 | TCP 1-RTT (+TLS) | TCP 1-RTT (+TLS) | **1-RTT（含加密）** |
| 头部压缩 | 无 | HPACK | QPACK |
| 网络切换 | 断连重连 | 断连重连 | ✅ **连接迁移，不断连** |

---

## 6. 到达服务器之前：CDN、反向代理、负载均衡

前面默认「请求直达源服务器」，但真实世界里，请求在到达业务代码之前，往往先经过好几层基础设施。

### 6.1 CDN：把内容搬到离用户最近的地方

回想第 1 节的结论：**RTT 由物理距离决定，且无法突破光速。** 如果你的服务器在美国，中国用户每个请求都要跨太平洋（RTT 150ms+），体验很差。**CDN（内容分发网络）** 的核心思想就是：**在全球各地部署大量边缘节点（edge），把静态内容（图片、CSS、JS、视频，乃至缓存的页面）复制到离用户最近的节点，用户就近取用，把长距离 RTT 变成短距离 RTT。**

「就近接入」通常靠两种技术实现：**基于 DNS 的调度**（DNS 解析时根据用户地理位置返回最近节点的 IP）或 **Anycast**（多个节点共用一个 IP，网络路由天然把用户导向最近的那个）。当边缘节点没有缓存所需内容时（缓存未命中），它会去**回源**——向源服务器请求一次并缓存下来，供后续用户直接命中。

CDN 带来的不只是快：它还挡在源站前面吸收了绝大部分流量（减轻源站压力）、抵御 DDoS、就近做 TLS 握手（缩短握手 RTT）。这也是为什么「静态资源全上 CDN」是几乎所有网站的标配。

### 6.2 反向代理与负载均衡

到达数据中心后，请求通常先撞上一个**反向代理（Reverse Proxy，如 Nginx）**。它对外是唯一入口，对内把请求转发给后面一堆真实的应用服务器。它顺手干很多活：TLS 终结（在这里解密，后端跑明文省 CPU）、静态文件直接返回、gzip 压缩、限流、缓存。

反向代理背后往往连着**负载均衡（Load Balancer）**，负责把海量请求**均匀分发**到多台后端服务器，避免某台被打爆。常见分发算法：

| 算法 | 做法 | 适用 |
|---|---|---|
| 轮询（Round Robin） | 依次轮流分给每台 | 后端配置一致时最简单 |
| 加权轮询（Weighted RR） | 按机器性能配权重，强的多分 | 后端配置不均时 |
| 最少连接（Least Connections） | 分给当前连接数最少的 | 请求处理时长差异大时 |
| IP Hash | 按客户端 IP 哈希固定分到某台 | 需要会话保持（同一用户总落同一台） |
| 一致性哈希（Consistent Hashing） | 哈希环分配，增删节点时只影响相邻 | 缓存类服务，减少节点变动导致的缓存失效 |

这一整套（CDN → 反向代理 → 负载均衡 → 应用服务器）就是「服务端处理时间」这段黑盒的展开。它们能让 TTFB 更短、系统更抗压。

---

## 7. 浏览器渲染：从字节流到屏幕像素

HTML 字节流终于回到浏览器了。但「拿到 HTML」离「屏幕上出现页面」还差一整套流程，这套流程叫**关键渲染路径（Critical Rendering Path）**。

### 7.1 六个步骤：解析 → 构建 → 布局 → 绘制 → 合成

```
HTML 字节流 ──解析──▶ DOM 树（文档对象模型，页面的结构）
                          │
CSS 字节流 ──解析──▶ CSSOM 树（每个节点的样式）
                          │
        DOM + CSSOM ──合并──▶ 渲染树（Render Tree，只含"要显示"的节点)
                          │
                    ──布局(Layout/Reflow)──▶ 算出每个节点的几何位置和大小（在哪、多大）
                          │
                    ──绘制(Paint)──▶ 把每个节点画成一层层像素（颜色、文字、边框、阴影）
                          │
                    ──合成(Composite)──▶ 把多个图层按正确顺序合并、交给 GPU 输出到屏幕
```

逐个说清楚每一步「在干什么」：

1. **构建 DOM**：浏览器边接收 HTML 字节，边把标签解析成一棵树状结构 DOM（Document Object Model），它是页面结构的内存表示。这个过程是**增量**的——不用等 HTML 全下载完就开始。
2. **构建 CSSOM**：把 CSS 解析成 CSSOM（CSS Object Model），记录每个节点最终的样式。注意 **CSS 是"渲染阻塞资源"**——因为在样式没算全之前渲染出来的东西可能是错的（比如背景色、布局全变），浏览器宁愿等 CSSOM 就绪也不愿先渲染再重画。所以慢的 CSS 会拖住首屏。
3. **合成渲染树**：把 DOM 和 CSSOM 合并。**关键：`display: none` 的节点不进渲染树**（它压根不占位、不显示）；而 `visibility: hidden` 的节点仍在渲染树里（它占位，只是看不见）。渲染树只包含「需要显示的内容」。
4. **布局（Layout / Reflow）**：计算渲染树里每个节点的**精确几何信息**——在视口的什么位置、多宽多高。这一步很贵，因为一个节点的位置可能牵动一大片。
5. **绘制（Paint）**：把每个节点转成实际的像素——填色、画文字、描边框、画阴影，通常分成多个图层。
6. **合成（Composite）**：把这些图层按正确的层叠顺序合并成最终画面，交给 GPU 快速输出到屏幕。像 `transform`、`opacity` 这类动画之所以流畅，正是因为它们**只触发合成、不触发布局和绘制**，可以走 GPU，不用重算几何。

### 7.2 为什么 JS 会阻塞渲染，以及 defer / async

HTML 解析过程中遇到 `<script>`（无标记的普通脚本）时，浏览器会**暂停 HTML 解析**，先下载并执行完这段 JS，再继续解析 HTML。这叫**解析器阻塞（parser-blocking）**。原因是：JS 可能通过 `document.write` 等改写文档结构，浏览器不敢一边改一边解析，只能停下来等它跑完。所以**把大 `<script>` 放在 `<head>` 里会严重拖慢首屏**——HTML 还没解析到 body，就卡在脚本上了。经典建议「把 script 放 body 底部」正是为此。

现代做法是用 `defer` 或 `async` 属性，让脚本**下载**不阻塞 HTML 解析：

| | 下载时机 | 执行时机 | 保证顺序？ |
|---|---|---|---|
| `<script>`（普通） | 遇到就下载 | 下载后**立即执行，阻塞解析** | — |
| `<script async>` | 并行下载，不阻塞解析 | **下载完立即执行**（可能打断解析），谁先下完谁先跑 | ❌ 不保证 |
| `<script defer>` | 并行下载，不阻塞解析 | 等 HTML **解析完成后、`DOMContentLoaded` 前**按顺序执行 | ✅ 保证书写顺序 |

选择：**有依赖关系、要按顺序跑、要操作 DOM 的脚本用 `defer`**（比如你自己的应用代码）；**独立、不依赖别人、不依赖 DOM 的用 `async`**（比如埋点统计脚本）。

顺带两个高频面试点——两个生命周期事件的区别：

- **`DOMContentLoaded`**：DOM 树构建完成、`defer` 脚本执行完就触发，**不等图片、CSS、字体等外部资源**。
- **`load`**：等页面**所有资源**（图片、样式、iframe……）都加载完才触发，通常晚得多。

### 7.3 预扫描器：浏览器偷偷提前下资源

前面说 JS 会阻塞解析，那岂不是解析一停，下面的图片、CSS 都不下载了？现代浏览器（Chrome 的 Blink 引擎、V8 执行 JS）有个聪明的优化叫**预加载扫描器（Preload Scanner）**：当主解析器被某个脚本卡住时，有一个轻量的辅助扫描器会**继续往后扫 HTML**，把后面出现的 CSS、JS、图片等资源的 URL 提前发现、提前开始下载。这样即使主解析被阻塞，网络也没闲着。这是浏览器为「阻塞」打的一个重要补丁，也是为什么把资源写在 HTML 里（而非 JS 动态插入）更利于被提前发现。

### 7.4 衡量渲染快不快：几个关键指标

- **TTFB（Time To First Byte）**：首字节到达时间，反映网络+后端。它是**诊断指标**（不属于 Core Web Vitals），但直接影响后面的 FCP、LCP。
- **FCP（First Contentful Paint）**：首次内容绘制——用户第一次看到**任何**内容（文字、图片）的时刻。同样是诊断指标。

真正被 Google 用来给网站体验打分的是 **Core Web Vitals（核心网页指标）**。这里有一个重要的时效性坑：**2024 年 3 月起，Core Web Vitals 三件套是 LCP / INP / CLS——原来的 FID 已被 INP 正式取代**，很多老文章还写 FID，别跟着错。

| 指标 | 衡量什么 | 「良好」阈值（75 分位） |
|---|---|---|
| **LCP**（Largest Contentful Paint，最大内容绘制） | 页面主要内容（通常是最大的图或标题块）渲染完成的时刻——「加载快不快」 | ≤ **2.5s** |
| **INP**（Interaction to Next Paint，交互到下次绘制） | 用户交互后页面响应的延迟——「点了有没有反应」（2024 年替代了 FID） | ≤ **200ms** |
| **CLS**（Cumulative Layout Shift，累积布局偏移） | 页面元素意外跳动的程度——「稳不稳、会不会点错」 | ≤ **0.1** |

这些指标把「渲染」和「交互」这些抽象过程变成了可量化、可优化的数字，是前端性能工作的抓手。

---

## 8. 性能视角：每一环能怎么优化

把整条链路铺开，每一环都有对应的优化手段。**通读这张表，你会发现绝大多数手段的本质，都是本文开头那句话——减少 RTT，或者把 RTT 藏起来（提前做/并行做/就近做/复用连接/干脆用缓存跳过）。**

| 环节 | 瓶颈所在 | 优化手段 | 本质 |
|---|---|---|---|
| **DNS 解析** | 冷查询要跑多级服务器（20~120ms） | `dns-prefetch`/`preconnect` 提前解析；合理设 TTL；用快的公共 DNS | 提前做 / 用缓存 |
| **TCP 握手** | 每次建连 1 RTT | Keep-Alive 复用连接；HTTP/2 一条连接跑多请求 | 复用，消灭重复往返 |
| **TLS 握手** | TLS 1.2 要 2 RTT | 升级 **TLS 1.3**（1 RTT）；会话恢复用 0-RTT；OCSP Stapling | 减少往返 |
| **建连整体** | 首次访问握手开销大 | **CDN 就近接入**（缩短每个 RTT）；HTTP/3（1-RTT 含加密） | 缩短 RTT / 合并往返 |
| **TTFB** | 后端处理慢 | 后端缓存、DB 优化、CDN 缓存整页、边缘计算 | 减少服务端处理时间 |
| **传输内容** | 体积大、传得慢 | gzip/Brotli 压缩；图片用 WebP/AVIF；按需加载；HTTP/2 多路复用 | 减少字节 + 并行 |
| **重复请求** | 没变的资源反复下载 | 强缓存(`Cache-Control`) + 协商缓存(`ETag`)；文件名带 hash 做长缓存 | 用缓存跳过往返 |
| **协议层** | HTTP/1.1 队头阻塞、6 连接上限 | 升级 **HTTP/2**（多路复用）→ **HTTP/3**（消灭 TCP 队头阻塞、弱网友好、连接迁移） | 消灭队头阻塞 |
| **弱网/移动** | 丢包、网络切换断连 | HTTP/3 的流独立重传 + 连接迁移 | 传输层解耦 |
| **渲染-CSS** | CSS 阻塞渲染 | 关键 CSS 内联、其余异步加载；减小 CSS 体积 | 缩短阻塞时间 |
| **渲染-JS** | JS 阻塞 HTML 解析 | `defer`/`async`；代码分割；脚本放底部；SSR 首屏直出 | 不阻塞解析 |
| **渲染-布局** | 频繁 reflow 卡顿 | 减少强制同步布局；动画用 `transform`/`opacity`（只触发合成） | 避免重排重绘 |
| **首屏体感** | 关键资源发现晚 | `<link rel="preload">` 关键资源；资源写进 HTML 让预扫描器提前发现 | 提前下载 |

---

## 9. 常见困惑澄清

### 9.1 「递归查询」和「迭代查询」到底谁递归谁迭代

再强调一次这个高频失分点：**递归发生在「客户端 ↔ 本地递归解析器」之间**——你只管要最终结果，解析器全权负责查到底。**迭代发生在「递归解析器 ↔ 各级 DNS 服务器（根/TLD/权威）」之间**——解析器一跳一跳地问，每个服务器只回「下一跳去哪问」而不直接给答案。方向记反就全错了。

### 9.2 为什么 HTTPS 首次慢，但整体不一定慢

HTTPS 首次建连比 HTTP 多 1~2 个 RTT 的握手（TLS）。但这只在**首次建连**付出，一旦连接建立并复用（Keep-Alive / HTTP/2 多路复用），后续请求都不再握手。而且 HTTPS 是 HTTP/2、HTTP/3 的前提（浏览器只在 HTTPS 上启用它们），HTTP/2 的多路复用带来的收益，往往远超那点握手开销。所以「HTTPS 慢」只在冷启动的第一个请求上成立，全局看现代 HTTPS 站点通常更快。

### 9.3 HTTP/2 已经多路复用了，为什么还需要 HTTP/3

因为 HTTP/2 只解决了**应用层**队头阻塞，没解决**传输层**（TCP）队头阻塞：所有流共用一条 TCP 连接，TCP 一旦丢包就得停下来等重传，把所有流一起卡住。HTTP/3 换用基于 UDP 的 QUIC，让每个流独立重传，才真正把队头阻塞连根拔起——**在丢包率高的弱网/移动场景，这个差别尤其明显**（HTTP/2 在弱网下甚至可能不如 HTTP/1.1）。

### 9.4 `no-cache` 不是「不缓存」

这是命名坑：`Cache-Control: no-cache` 的意思是**「可以缓存，但每次用之前必须去服务器验证」**（走协商缓存），真正的「完全不缓存」是 **`no-store`**。想让敏感数据不落地，用 `no-store`，别用 `no-cache`。

### 9.5 `display:none` 和 `visibility:hidden` 在渲染上的区别

`display: none` 的节点**不进渲染树**——不占位、不布局、不绘制，改它会触发重排。`visibility: hidden` 的节点**仍在渲染树里**——正常占位和布局，只是绘制成「透明看不见」。所以前者是「彻底不存在于布局」，后者是「占着位置但隐身」，这在性能和布局行为上是两回事。

### 9.6 `defer` 和 `async` 别用混

都不阻塞 HTML 解析的**下载**，区别在**执行**：`async` 下载完立刻执行（可能打断解析、且多个 async 谁先下完谁先跑，**不保证顺序**）；`defer` 等 HTML 解析完再**按书写顺序**执行（在 `DOMContentLoaded` 之前）。要顺序、要操作 DOM 的用 `defer`；纯独立的埋点/统计用 `async`。

---

## 10. 一句话收尾

如果这篇长文只能留下一句话，那就是：

> **一个 HTTP 请求的一生，大部分时间不是在「算」，而是在「等」——等 DNS 查完、等握手来回、等阻塞的资源下完。每一次网络往返（RTT）都是一堵受光速限制、花钱也买不穿的墙。所以从 DNS 预解析、连接复用、TLS 1.3、HTTP/3，到 CDN 就近接入、缓存、`defer`——这条链路上几乎所有优化，本质都是同一件事：减少往返的次数，或者把往返藏在用户察觉不到的地方。**

下次再被问到「输入 URL 之后发生了什么」，你不用背流程了——顺着「解析地址 → 建立连接 → 收发数据 → 渲染画面」这四步走，每一步问自己「这里有几个 RTT，能不能省」，整条链路和它的优化就都长在同一棵逻辑树上了。
