外观
从输入网址到看到页面:一次网页加载的完整旅程
你在地址栏敲下 https://example.com,按下回车。
到第一个像素出现在屏幕上,通常只有 几百毫秒。这几百毫秒里,至少发生了七件大事:域名解析、建连、加密、发请求、服务端处理、拿数据、画页面。
这篇文章把这条链路完整走一遍。不是面试八股文——每一站我都会说明它为什么必须存在,以及少了它会怎样。
0. 先看全貌
| 站 | 发生了什么 | 关键协议 | 典型耗时 |
|---|---|---|---|
| ① DNS | 域名 → IP 地址 | DNS / DoH | 0–100ms(命中缓存则 ~0) |
| ② TCP | 三次握手建立连接 | TCP | 1 个 RTT |
| ③ TLS | 协商密钥、验证证书 | TLS 1.3 | 1 个 RTT |
| ④ HTTP | 发请求、收响应 | HTTP/2、HTTP/3 | 1 个 RTT |
| ⑤ 服务端 | 查库、算数据、拼 HTML | — | 10–300ms |
| ⑥ 传输 | HTML/CSS/JS/图片陆续到达 | TCP / QUIC | 若干 RTT |
| ⑦ 渲染 | 解析、布局、绘制、合成 | — | 数十至数百 ms |
RTT(Round-Trip Time,往返时延):一个包从你这儿到服务器再回来一趟的时间。国内访问国内服务器约 10–30ms,跨洋到美西约 150–200ms。上面每一"站"的耗时,几乎都是 RTT 的整数倍——这就是为什么"离用户近"是 Web 性能的第一性原理。
1. 第一站:DNS —— 把域名翻译成 IP
计算机之间只认 IP。example.com 这个名字,必须先变成 93.184.216.34 这样的地址。
查找顺序是层层缓存,逐级上报:
- 浏览器缓存——浏览器自己记得最近访问过哪些域名;
- 系统缓存——操作系统的 DNS 缓存(还有
hosts文件); - 本地解析器(resolver)——通常是运营商或你设置的公共 DNS,如
114.114.114.114、8.8.8.8; - 如果解析器也没有,它开始向上递归查询:
- 问根域名服务器:
.com归谁管? - 问顶级域(TLD)服务器:
example.com归谁管? - 问权威域名服务器:
example.com的 A 记录是什么?
- 问根域名服务器:
前三级是递归("我不知道,但我帮你问到"),后三级是迭代("我不知道,你去问它")。最终结果会一路缓存回来,所以第二次访问基本是 0 耗时。
常见记录类型
| 类型 | 作用 |
|---|---|
A | 域名 → IPv4 地址 |
AAAA | 域名 → IPv6 地址 |
CNAME | 域名 → 另一个域名(别名,如 CDN 域名) |
NS | 这个域由哪些权威服务器负责 |
TXT | 任意文本,常用于域名验证、SPF 反垃圾邮件 |
两个容易踩的坑
- CNAME 不能放在根域。规范上
example.com不能是 CNAME(否则和 MX、NS 等记录冲突),所以常见做法是给www做 CNAME,根域用 A 记录或提供商自己的 ALIAS/ANAME。 - TTL 不是"越快越好"。TTL 是缓存有效期。切机房、换 CDN 前要把 TTL 提前调小,否则全国缓存还在指向旧 IP,切换半天不生效。
顺带一提:DNS 默认是明文的 UDP,可能被篡改或监听。DoH(DNS over HTTPS)和 DoT 把 DNS 包塞进 TLS 里,隐私和完整性更好,代价是本地缓存失效更快。
2. 第二站:TCP 三次握手
拿到 IP 后,浏览器要和服务器建立可靠连接。TCP 是面向连接的:正式传数据前,双方必须先同步序列号,确认"你能收到我、我也能收到你"。
客户端 服务器
| ── SYN, seq=x ───────────────────▶ | 我想连,我的序号从 x 开始
| ◀── SYN + ACK, seq=y, ack=x+1 ──── | 我也想连,我的序号从 y;x+1 我收到了
| ── ACK, seq=x+1, ack=y+1 ────────▶ | y+1 我也收到了,开始吧为什么是三次,不是两次?
因为要双方各自确认一次"对方能收、我能发"。两次握手只能让服务器确认"客户端能发、我能收",但客户端始终没确认"服务器能发、我能收"——万一服务器的 SYN+ACK 丢在半路,连接就是个单向死链。第三次 ACK 补上了这个方向。
序列号(seq)是干嘛的? TCP 要解决丢包、乱序、重复。给每个字节编号,接收方才能"缺哪段补哪段、顺序乱了重排"。握手的关键任务就是把双方的初始序列号(ISN)告诉对方。
谁在监听
80(HTTP)、443(HTTPS)是服务端端口。你本地连出去用的是临时端口,通常由系统自动分配。
3. 第三站:TLS 握手(HTTPS)
连接建立后,如果是 https://,还要再协商一层加密。没有 TLS,你发出去的请求内容、Cookie、表单数据都是明文,链路上任何一跳都能看到甚至改动。
TLS 1.3 的握手只要 1 个 RTT(1.2 要 2 个):
Client Server
| ── ClientHello ─────────────────▶ | 我支持的 TLS 版本、密码套件、密钥共享参数
| ◀─ ServerHello ────────────────── | 选定套件、它的密钥共享参数
| ◀─ {Certificate} ──────────────── | 服务器证书(含公钥)+ 证书链
| ◀─ {Finished} ─────────────────── | 握手消息的校验值
| ── {Finished} ─────────────────▶ | 同上
| ══ 加密的应用数据(HTTP)════════▶ |大括号表示"这段已加密"。
三件事在这一站完成
- 协商算法:用哪套密钥交换、对称加密、哈希——双方挑一个都支持的最强的;
- 验证身份:服务器发来证书,浏览器顺着证书链一路验到内置根证书,确认域名匹配、证书在有效期内、未被吊销。这一环断了,就是你看到的"不安全连接"警告;
- 协商出会话密钥:靠 ECDHE(椭圆曲线迪菲-赫尔曼)做密钥交换——双方各自生成临时密钥对,交换公钥,各自算出同一个共享密钥。即使私钥日后泄露,过去的会话也推不出来(前向保密)。
为什么 TLS 1.3 快了
1.2 时代要两个 RTT:先交换 Hello 和证书,客户端验证完证书再发密钥材料。1.3 把"客户端猜服务器会用哪套参数"变成默认行为,客户端在第一个包里就把密钥共享材料一并发出,省掉一整趟。
想更快要上 TLS 1.3 的 0-RTT:恢复会话时客户端第一个数据包就带应用数据。但 0-RTT 数据可被重放攻击,绝不能用于非幂等请求(下单、支付)。多数站点只对静态 GET 开 0-RTT。
4. 第四站:HTTP 请求与响应
加密通道打通,终于可以说正事了。
一次请求长什么样
GET /articles/from-url-to-pixels/ HTTP/2
Host: jinghuasociety-scripturelibrary.asoe.cn
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml,...
Accept-Encoding: br, gzip
Cookie: session=...响应则是状态行 + 响应头 + 响应体:
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=600
content-encoding: br该记住的状态码
| 码 | 含义 | 实务要点 |
|---|---|---|
| 200 | 成功 | — |
| 301 | 永久重定向 | 浏览器和搜索引擎会记住,权重转移到新地址 |
| 302 | 临时重定向 | 不转移权重 |
| 304 | 协商缓存命中 | 资源没变,直接用本地副本,不传 body |
| 400 / 403 / 404 | 请求错 / 禁止 / 找不到 | 404 要返回 404 状态码,别返回 200+错误页 |
| 500 | 服务端炸了 | 看日志,别猜 |
| 502 / 504 | 网关错 / 网关超时 | 多半是后面应用挂了或太慢 |
HTTP/2 与 HTTP/3 改了什么
- HTTP/1.1:一个连接同时只跑一个请求,浏览器只好开 6 个并行连接。于是有了雪碧图、域名分片这些"妥协式优化"。
- HTTP/2:二进制分帧 + 多路复用。一个 TCP 连接上并行跑多个请求/响应,按流(stream)区分,互不阻塞;HPACK 压缩头部。上面那些妥协式优化基本可以退役了。
- HTTP/3:换成 QUIC over UDP。把"可靠传输 + 安全"搬到用户态,流与流之间真正独立——一个包丢了只影响它所在的那条流,不会堵住整个连接。弱网、移动网络下优势明显。
注意 HTTP/2 的一个遗留问题:多路复用在传输层仍是单条 TCP。丢包时 TCP 的重传会阻塞所有流——这就是 HTTP/3 要换掉 TCP 的动机。
5. 第五站:服务端做了什么
请求到达服务器,典型路径:
防火墙 / 安全组
↓
反向代理(Nginx / Caddy)—— TLS 终结、静态资源、限流、gzip/br
↓
应用服务(Node / Java / Go / PHP ...)
↓
缓存(Redis)→ 数据库(MySQL / PostgreSQL)→ 对象存储 / 消息队列几个决定体感的点:
- 静态资源尽量别进应用层。让 Nginx/CDN 直接吐,应用只处理动态请求;
- 数据库是常见的瓶颈。索引没建好,一条查询能把 CPU 打满;
- 缓存要讲策略。缓存雪崩、穿透、击穿是三个不同的故障,别混为一谈;
- 超时要设。不给下游设超时,一个慢依赖能拖垮整个服务。
6. 第六站:浏览器渲染
HTML 到手,浏览器的渲染管线开工:
HTML ──解析──▶ DOM ─┐
├──▶ Render Tree ──▶ Layout ──▶ Paint ──▶ Composite ──▶ 屏幕
CSS ──解析──▶ CSSOM┘- DOM:把 HTML 文本解析成节点树;
- CSSOM:解析 CSS,算出每个节点的样式;
- Render Tree:DOM + CSSOM,只留可见节点(
display: none的不在里面); - Layout(布局/重排):计算每个节点的确切位置和尺寸;
- Paint(绘制):把节点拆成绘制指令,填像素;
- Composite(合成):把多个图层交给 GPU 合成,最终上屏。
什么会阻塞渲染
<head>里同步的 CSS:渲染被挡,因为没样式就没法建 Render Tree;- **没有
defer/async的<script>:解析 HTML 被挡,因为脚本可能改 DOM 也可能document.write; @import的 CSS:串行再加一层请求;- 同步的字体加载。
所以工程上的常规操作是:CSS 放头部并压缩,JS 加 defer/async,关键路径内联,非关键资源懒加载。
defer和async的区别:defer保证按顺序在文档解析完后执行;async谁先下载完谁先执行,适合互不依赖的独立脚本(如统计)。
重排与重绘
- 重排(reflow):几何属性变了(宽高、位置),要重新 Layout,代价大;
- 重绘(repaint):只改颜色背景,重新 Paint,代价小;
- 合成(composite only):只改
transform/opacity,GPU 直接合成,最省。
想让动画顺滑,优先动
transform和opacity,别动width/top/left。
7. 第七站:复用与断开
一次请求结束,连接通常不关——Connection: keep-alive 让同一条 TCP 连接继续服务后续请求,省掉握手和慢启动。
真要断开时是四次挥手:
A ── FIN ──▶ B 我要说完话了(A 进入 FIN_WAIT_1)
A ◀─ ACK ── B 知道了(B 进入 CLOSE_WAIT)
A ◀─ FIN ── B 我也说完了(B 进入 LAST_ACK)
A ── ACK ──▶ B 好,再见(A 进入 TIME_WAIT)为什么最后要等 TIME_WAIT? 因为最后一次 ACK 可能丢。A 多等 2×MSL(最长报文段寿命),确保 B 没收到重传时还能补上。TIME_WAIT 太多会占端口,高并发短连接服务端常见这个烦恼。
8. 一张时间线收尾
把整条链路压缩成一行(以国内 CDN、HTTP/2 为例):
0ms 按下回车
~0ms DNS 命中缓存(或 +20ms 查本地解析器)
~20ms TCP 三次握手完成(1 RTT)
~35ms TLS 1.3 握手完成(再 1 RTT)
~50ms 请求到达服务端,开始查库拼 HTML
~90ms 首字节返回(TTFB),HTML 开始下载
~120ms 关键 CSS 到达,首次渲染
~200ms 首屏可见(LCP)
~800ms 图片与非关键 JS 陆续到位,页面可交互真实世界里,这几个数字经常被拖垮:DNS 慢了、证书链多了、TTFB 高了、关键 CSS 被一个第三方脚本堵住、图片没压缩。性能优化的本质,就是在这条时间线上找那一段不合理的长条。
9. 常见误区自查
- ❌ "HTTPS 比 HTTP 慢很多" —— TLS 1.3 只多 1 个 RTT,且有会话复用与 0-RTT;
- ❌ "HTTP/2 了就不需要开多个域名" —— 恰恰相反,多域名会破坏多路复用的收益;
- ❌ "返回 200 的错误页没问题" —— 搜索引擎和监控都靠状态码判断;
- ❌ "重排重绘一样糟" —— 合成最省,重排最贵;
- ❌ "TCP 两次握手够了" —— 少确认一个方向,可能建成单向连接;
- ❌ "关掉 DNS 缓存能加速" —— 只会让每次访问都多几次网络往返。
写在最后
这条链路值得完整走一遍,不是因为能背下来,而是因为每次网页"感觉慢",你都能定位到是哪一站出了问题:是 DNS 没缓存、TTFB 太高、关键路径被堵,还是图片太大。
把每一站都当成可以被质疑的环节,性能优化就不再是玄学。
本文所有协议细节基于 TLS 1.3 / HTTP/2 / HTTP-3 的现行规范。协议在演进,结论也会过时——会注明版本,因为版本一换,很多经验就作废。