10 · 网络协议与缓存(详解版)
一句话定位:前端所有数据都通过网络到达。协议决定「多快、多可靠」,缓存决定「要不要再跑一趟」,鉴权决定「谁能拿」。这三件事共同决定了页面的真实速度与安全边界。
读完本篇你应该能做到
- 看懂一次请求在 Network 面板里的每一段耗时,并判断瓶颈在 DNS、连接、服务端还是传输;
- 正确配置 HTTP 缓存、CDN 缓存与跨域,避免「更新不生效」和「预检失败」;
- 说清 HTTP/1.1、HTTP/2、HTTP/3 的差异与适用场景;
- 设计一套可解释的鉴权与缓存方案,并能对安全与成本负责。
0. 领域导论
0.1 这个领域解决什么问题
| 现象 | 根因 | 归属 |
|---|---|---|
| 接口 P99 800ms,后端说「我只用了 50ms」 | 连接复用差、跨地域、TLS 握手、排队 | 链路耗时 |
| 带 Cookie 的跨域请求全部失败 | Access-Control-Allow-Origin: * 与 credentials 互斥 | CORS |
| 发布后部分用户仍是旧页面 | HTML 被缓存,或 Service Worker 未更新 | 缓存策略 |
| 用户从第三方站点跳转后登录态丢失 | SameSite=Lax/Strict 拦截了跨站 Cookie | Cookie 属性 |
网络问题最难的地方是:它不在你的代码里。你必须能读懂协议、读懂面板、读懂响应头。
0.2 一个类比
把一次请求想成寄快递:
- DNS = 查地址(把域名翻译成 IP)。
- TCP/TLS 握手 = 打电话确认收件、核对身份(HTTPS 的证书校验)。
- HTTP 请求 = 填快递单(方法、路径、头、体)。
- CDN = 前置仓:货在离你最近的仓库,直接送。
- 缓存 = 家里已有的存货:没过期就直接用,不用再买。
- 鉴权 = 取件码:没有码,仓库不放货。
理解了这条链路,你就知道「为什么首屏慢不一定是接口慢」——可能是查地址慢、打电话慢、仓库远。
0.3 学习地图
10 网络协议与缓存
├─ 前置:05 浏览器(同源、存储、渲染)
├─ 紧接:11 性能(TTFB/LCP)、12 安全(CORS/XSS/CSRF)
├─ 交叉:14 BFF(服务端转发与聚合)、09 构建(资源 hash)
└─ 长期:17 架构(多地域与容灾)
0.4 分级标尺
| 级别 | 能力特征 | 典型产出 | 自检标准 |
|---|---|---|---|
| L1 基础 | 会用 fetch、看状态码、配简单跨域 | 接口联调 | 能定位 404/500/CORS 报错 |
| L2 进阶 | 懂缓存协商、HTTP/2、鉴权方案 | 缓存与鉴权配置 | 能解释 304 与预检 |
| L3 高级 | 懂 TLS/QUIC、连接复用、实时通信 | 链路优化、协议选型 | 能读懂瀑布图每一段 |
| L4 专家 | 建缓存体系、多地域容灾、成本治理 | CDN/网关方案、SLO | 能对可用性与成本负责 |
1. L1 基础:先看懂一次请求
1.1 报文长什么样
GET /api/orders?page=1 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer xxx
Cookie: sid=abc
If-None-Match: "v123"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: private, max-age=60
ETag: "v124"
Content-Length: 245
{"list":[],"total":0}
三部分:起始行(方法/路径/状态)、头(元信息)、体(数据)。前端 90% 的问题都能从「头」里找到答案。
1.2 方法与状态码
| 方法 | 语义 | 幂等 | 可缓存 |
|---|---|---|---|
| GET | 读取 | 是 | 是 |
| POST | 创建/提交 | 否 | 极少 |
| PUT | 全量替换 | 是 | 否 |
| PATCH | 局部更新 | 不一定 | 否 |
| DELETE | 删除 | 是 | 否 |
| 状态码 | 含义 | 前端常见处理 |
|---|---|---|
| 200 / 201 | 成功 / 已创建 | 正常渲染 |
| 204 | 成功无内容 | 不要再解析 body |
| 301 / 302 | 永久/临时重定向 | 注意 POST 变 GET |
| 304 | 协商缓存命中 | 用本地副本 |
| 400 / 422 | 参数/校验错误 | 展示字段级错误 |
| 401 | 未认证 | 跳登录或刷新 token |
| 403 | 无权限 | 展示无权限页,不要跳登录 |
| 404 | 不存在 | 空状态/404 页 |
| 429 | 限流 | 退避重试 + 提示 |
| 500 / 502 / 504 | 服务端/网关错误 | 兜底 + 上报 |
401 与 403 的区别是面试高频题,也是产品体验的分水岭:401 是「你是谁我不知道」,403 是「我知道你是谁,但你不能看」。
1.3 必须认识的请求头
Accept: application/json # 期望的响应类型
Content-Type: application/json # 请求体的类型
Authorization: Bearer <token> # 令牌
Cookie: sid=abc; theme=dark # 自动携带
Origin: https://app.example.com # 跨域来源(浏览器自动加)
Referer: https://app.example.com/list # 来源页面
If-None-Match: "v123" # 协商缓存
Accept-Encoding: gzip, br # 支持的压缩
1.4 必须认识的响应头
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=31536000, immutable
ETag: "v124"
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
Vary: Accept-Encoding
Access-Control-Allow-Origin: https://app.example.com
Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax
Content-Encoding: br
1.5 同源与跨域:为什么会被拦
同源 = 协议 + 域名 + 端口三者完全相同。
| 页面 | 请求 | 同源? | 原因 |
|---|---|---|---|
https://a.com | https://a.com/api | 是 | 全部一致 |
https://a.com | http://a.com/api | 否 | 协议不同 |
https://a.com | https://api.a.com | 否 | 域名不同 |
https://a.com | https://a.com:8443 | 否 | 端口不同 |
关键认知:跨域是浏览器的限制,不是服务器的限制。请求可能已经发出、服务器也处理了,只是浏览器不给你读响应。
1.6 简单请求与预检
简单请求(不触发 OPTIONS):
方法 ∈ {GET, HEAD, POST}
且 Content-Type ∈ {text/plain, multipart/form-data, application/x-www-form-urlencoded}
且 无自定义头
其余 → 先发 OPTIONS 预检,通过后才发真实请求
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
四个必记规则:
Allow-Origin为*时不能带 Cookie(credentials);- 带 credentials 时必须回显具体来源,且响应要
Allow-Credentials: true; - 非简单响应头(如
X-Total-Count)需要Access-Control-Expose-Headers才能被 JS 读到; Max-Age内不重复预检,能显著减少请求数。
1.7 Cookie 与前端存储的分工
| 存储 | 是否自动随请求发送 | 容量 | 能否被 JS 读 | 适合 |
|---|---|---|---|---|
| Cookie | 是(同源/符合 SameSite) | 约 4KB | HttpOnly 时不能 | 会话标识 |
localStorage | 否 | 约 5MB | 能 | 偏好、非敏感缓存 |
sessionStorage | 否 | 约 5MB | 能 | 单标签页会话 |
| IndexedDB | 否 | 大 | 能 | 结构化离线数据 |
安全建议:会话令牌放 HttpOnly; Secure; SameSite=Lax 的 Cookie,比放 localStorage 更抗 XSS 窃取。
1.8 HTTPS 到底做了什么
| 能力 | 说明 |
|---|---|
| 机密性 | 传输加密,中间人看不到内容 |
| 完整性 | 被篡改能被发现 |
| 身份认证 | 证书证明「你连的确实是这个站」 |
前端要关心的:混合内容(HTTPS 页面加载 HTTP 资源会被拦)、HSTS(强制 HTTPS)、证书过期(全站白屏)。
1.9 缓存键与 Vary:缓存的「身份」
缓存不是按 URL 存一份,而是按缓存键存多份。键算错,就会出现「A 看到 B 的内容」。
缓存键 ≈ 方法 + 规范化 URL + Vary 指定的请求头
| 响应头 | 作用 | 常见误用 |
|---|---|---|
Vary: Accept-Encoding | 区分压缩格式 | 几乎必加 |
Vary: Cookie | 按 Cookie 区分 | 会让缓存碎片化,通常应改为按业务 key |
Vary: User-Agent | 区分设备 | 取值太多,命中率骤降 |
Cache-Control: private | 只允许浏览器缓存 | CDN 不应缓存 |
s-maxage | 共享缓存(CDN)的 TTL | 与 max-age 混用导致浏览器/CDN 不一致 |
stale-while-revalidate | 先给旧内容,后台更新 | 对强一致页面不适用 |
URL 规范化:?a=1&b=2 与 ?b=2&a=1、尾斜杠、大小写、utm_* 参数,都会产生不同缓存键。CDN 侧要统一排序、剔除无关参数,否则命中率会被无声地拉低。
缓存投毒:攻击者构造一个带恶意头的请求,让 CDN 缓存了错误响应,所有用户都拿到。防御要点:只按白名单头做键、不回显未校验的请求头、对 Host/X-Forwarded-* 严格校验。
前端可控的部分:
// 浏览器缓存策略:强制缓存 + 过期后协商
fetch('/api/config', { cache: 'force-cache' }); // 优先用缓存
fetch('/api/user', { cache: 'no-store' }); // 不缓存(含隐私数据)
fetch('/api/list', { cache: 'no-cache' }); // 每次都协商
判断缓存是否生效:看响应头的 Age、X-Cache: HIT/MISS、CF-Cache-Status;不要只看「第二次快了一点」。
1.10 L1 小结与自检
| 主题 | 关键结论 |
|---|---|
| 报文 | 起始行 + 头 + 体,问题多在头里 |
| 状态码 | 401 与 403 语义不同,处理也不同 |
| 跨域 | 浏览器限制;预检决定能否发真实请求 |
| Cookie | 自动携带,但要配好 Secure/SameSite |
| HTTPS | 加密 + 完整性 + 身份 |
自检题
- 为什么跨域请求「后端日志里有,前端却报错」?——请求已发出并被服务端处理,但响应缺少合法的 CORS 头,浏览器拦截了读取;看 Network 的 CORS 报错与响应头即可确认。
Allow-Origin: *为什么不能配credentials?——规范禁止通配符与凭证共存,否则任意站点都能带着用户 Cookie 读取响应;必须回显具体 Origin 并做白名单校验。- 401 和 403 的前端处理为什么不能一样?——401 是未认证,应刷新 token 或跳登录;403 是已认证但无权限,应提示无权限,反复登录没有意义。
- 为什么
Vary: User-Agent会降低缓存命中率?——UA 取值极多,每个细微版本都会生成新键,缓存被切得极碎;应只按设备类型等少量枚举值区分。
2. L2 进阶:缓存、协议与鉴权
2.1 HTTP 缓存的两条路径
强缓存:Cache-Control / Expires
命中 → 不发请求,直接用本地副本(200 from disk cache)
协商缓存:ETag / Last-Modified
未命中强缓存 → 带 If-None-Match / If-Modified-Since 询问
→ 未变:304(不传 body,省带宽)
→ 已变:200 + 新内容
# 强缓存 1 年,内容不可变(配合 hash 文件名)
Cache-Control: public, max-age=31536000, immutable
# HTML:每次都验证,保证发布即时生效
Cache-Control: no-cache
# 完全不允许缓存(含中间代理)
Cache-Control: no-store
# 过期后先用旧的,同时后台刷新
Cache-Control: max-age=60, stale-while-revalidate=600
| 指令 | 含义 | 常见误用 |
|---|---|---|
max-age | 新鲜期(秒) | 给 HTML 设太长导致发版不生效 |
no-cache | 可以存,但每次要验证 | 被误解为「不缓存」 |
no-store | 完全不许存 | 用在所有接口上,失去缓存收益 |
immutable | 新鲜期内不验证 | 用在会变的 URL 上 |
private | 仅浏览器可缓存 | 与 CDN 共享缓存冲突 |
s-maxage | 共享缓存(CDN)新鲜期 | 与 max-age 混用导致行为不一致 |
经典组合:
index.html → no-cache(每次都验证)
/assets/*.a1b2.js → max-age=31536000, immutable(文件名带 hash)
/api/list → private, max-age=30, stale-while-revalidate=60
2.2 一次带缓存的完整时序
1. 浏览器查强缓存:新鲜 → 直接用(200 from cache),结束
2. 不新鲜 → 带 If-None-Match: "v1" 发请求
3. 服务端比对:一致 → 304(无 body)
4. 浏览器用本地副本渲染
5. 不一致 → 200 + 新 ETag + 新内容
为什么 304 仍然是一次往返:它省的是带宽,不是延迟。要省延迟必须靠强缓存或 CDN 就近返回。
2.3 CDN 缓存与回源
用户 → 边缘节点(L1)
├─ 命中 → 直接返回(几毫秒)
└─ 未命中 → 回源站(几十到几百毫秒)→ 缓存 → 返回
| 概念 | 含义 | 配置要点 |
|---|---|---|
| TTL | 边缘缓存时长 | 静态资源长、HTML 短 |
| 回源 | 边缘未命中时向上游取 | 控制回源带宽成本 |
| 预热 | 发版前主动推送到边缘 | 避免首批用户慢 |
| 刷新/purge | 主动失效 | 发版后立即生效 |
| 缓存键 | 用什么区分缓存 | 注意不要把 Cookie 纳入(会击穿命中率) |
最常见事故:CDN 把带用户信息的响应缓存成公共副本。防护:Cache-Control: private/no-store + 缓存键不含身份。
2.4 HTTP/1.1 的瓶颈
| 问题 | 表现 | 影响 |
|---|---|---|
| 队头阻塞 | 前一个响应没完,后面排队 | 并发受限 |
| 明文头部 | 每个请求重复几百字节 | 浪费带宽 |
| 连接数限制 | 每域名 6 个左右 | 资源多时排队 |
HTTP/1.1 时代的优化手段(现在很多已过时):雪碧图、域名分片、资源合并、内联。HTTP/2 之后这些多数变成反模式。
2.5 HTTP/2:多路复用
一条 TCP 连接
├─ Stream 1: /index.html
├─ Stream 3: /app.js
└─ Stream 5: /api/list
(帧交错传输,互不阻塞)
| 特性 | 收益 | 注意 |
|---|---|---|
| 多路复用 | 一个连接并发多请求 | TCP 层仍可能队头阻塞 |
| 头部压缩(HPACK) | 减少重复头 | 动态表有状态 |
| 二进制分帧 | 解析更高效 | 抓包需解码 |
| 服务器推送 | 主动推资源 | 已被主流废弃 |
| 流优先级 | 关键资源优先 | 实现差异大 |
对构建的影响:不再需要为了减少请求而合并一切;按路由拆包反而更优(缓存粒度更细)。
2.6 HTTP/3 与 QUIC
HTTP/2 over TCP:丢一个包 → 整条连接等重传(TCP 队头阻塞)
HTTP/3 over QUIC(UDP):每个流独立重传 → 只影响丢包的那条流
| 维度 | HTTP/2 | HTTP/3 |
|---|---|---|
| 传输层 | TCP + TLS 1.2/1.3 | QUIC(UDP 上实现) |
| 握手 | TCP + TLS 两次往返 | 合并,1-RTT,可 0-RTT |
| 丢包影响 | 全连接阻塞 | 仅单流 |
| 连接迁移 | IP 变则断 | 支持(连接 ID) |
| 部署 | 广泛 | 需要 UDP 放行,部分网络受限 |
前端能做的:确保 CDN/网关支持并开启;不要在客户端硬编码协议;用 Alt-Svc 让浏览器自动升级。
2.7 鉴权方案的取舍
| 方案 | 令牌存哪 | 优点 | 风险 |
|---|---|---|---|
| Session + Cookie | HttpOnly Cookie | 服务端可撤销、抗 XSS 窃取 | 需要 CSRF 防护、跨域配置复杂 |
| JWT(localStorage) | JS 可读 | 无状态、易跨服务 | XSS 可直接偷走;无法主动失效 |
| JWT(Cookie) | HttpOnly Cookie | 兼顾无状态与抗窃取 | 仍需 CSRF 防护 |
| OAuth2 + PKCE | 授权码换 token | 第三方授权标准 | 流程复杂,回调安全要求高 |
JWT 的真相:它不是「更安全的 Session」,而是「把状态放到客户端」的权衡。无法即时吊销是它的结构性缺陷,缓解手段是短过期 + 刷新令牌 + 黑名单。
/**
* 刷新令牌的并发控制:多个请求同时 401 时,只刷新一次。
* 否则会出现「刷新风暴」,把旧 refresh token 用废。
*/
let refreshing = null;
async function request(url, options = {}) {
let res = await fetch(url, { ...options, credentials: 'include' });
if (res.status === 401) {
refreshing ??= fetch('/api/refresh', { method: 'POST', credentials: 'include' })
.finally(() => { refreshing = null; });
const ok = await refreshing;
if (ok.ok) res = await fetch(url, { ...options, credentials: 'include' });
else location.replace('/login');
}
return res;
}
2.8 CORS 在工程中的三个落点
- 开发环境:Vite/Webpack devServer 代理,避免跨域(最省事);
- 生产同域:BFF 或网关统一域名,天然同源;
- 确实跨域:精确配置
Allow-Origin白名单 +Allow-Credentials+ 预检缓存。
不要用「关闭浏览器安全策略」解决跨域——那只是把问题留到线上。
2.9 令牌放哪里:XSS 与 CSRF 的权衡
| 存放位置 | XSS 能否读取 | CSRF 风险 | 跨域成本 | 适用 |
|---|---|---|---|---|
localStorage | 能(脚本可读) | 低(不会自动携带) | 低 | 同源 SPA 的短期方案 |
| 内存变量 | 刷新即失,攻击窗口小 | 低 | 低 | 访问令牌 + 静默刷新 |
httpOnly Cookie | 不能 | 高(需 SameSite/CSRF token) | 中(需 credentials) | Web 主流的稳妥方案 |
| BFF 会话 | 不能 | 低(同源) | 低 | 同构/BFF 架构 |
核心结论:没有「零风险」的存放位置,只有把风险放到更难利用的一侧。localStorage 把风险交给 XSS;httpOnly Cookie 把风险交给 CSRF,而 CSRF 可以用 SameSite=Lax/Strict + CSRF token 防住。
推荐组合:
访问令牌:内存(短期,5–15 分钟)
刷新令牌:httpOnly + Secure + SameSite=Lax Cookie
刷新接口:校验 Origin/Referer + CSRF token
并发刷新是工程上最容易出 bug 的地方:多个请求同时 401,会触发多次刷新,导致令牌互相失效。
/**
* 单飞刷新:同一时刻只允许一个刷新请求,其余请求排队等待。
* @param {() => Promise<string>} refresh 刷新函数,返回新 token
*/
function createTokenManager(refresh) {
let pending = null;
return {
async get() {
if (pending) return pending;
pending = refresh().finally(() => { pending = null; });
return pending;
},
};
}
四个常见坑:
| 坑 | 后果 | 修复 |
|---|---|---|
刷新令牌也放 localStorage | XSS 一次即长期失守 | 刷新令牌放 httpOnly Cookie |
| 401 时无限重试 | 请求风暴、账号被锁 | 刷新失败即登出,限制重试次数 |
| 刷新期间旧请求继续发 | 令牌错乱、部分失败 | 请求排队,刷新后统一重放 |
| 登出只清本地 | 令牌仍可用 | 服务端吊销 + 刷新令牌轮换 |
2.10 L2 小结与自检
| 主题 | 关键结论 |
|---|---|
| 强缓存 | 不请求;immutable + hash 文件名 |
| 协商缓存 | 304 省带宽不省往返 |
| CDN | 命中率取决于 TTL 与缓存键 |
| HTTP/2 | 多路复用,合并资源不再是默认优化 |
| HTTP/3 | QUIC 解决 TCP 队头阻塞 |
| 鉴权 | Cookie 抗 XSS,JWT 无状态但难吊销 |
自检题
no-cache和no-store的区别?各自适合什么资源?——no-cache可以缓存但每次使用前必须校验,适合 HTML;no-store完全不落地,适合敏感接口与个人数据。- 为什么 HTTP/2 下「雪碧图/合并 JS」可能反而更差?——多路复用让请求数不再是大瓶颈,合并会降低缓存粒度、让首屏加载无用字节,还会阻塞关键资源。
- JWT 无法即时失效,业务上怎么缓解?——缩短有效期加 refresh token,服务端维护黑名单或用户版本号,权限变更用短 TTL 或推送失效。
- 为什么「访问令牌放内存、刷新令牌放 httpOnly Cookie」更稳?——访问令牌短期且脚本可读窗口小;刷新令牌不可被 XSS 读取,配合 SameSite 与 CSRF token 抵御 CSRF,兼顾两类攻击。
3. L3 高级:握手、实时通信与链路优化
3.1 TLS 1.3 握手为什么快
TLS 1.2:TCP 三次握手 → TLS 两次往返 → 才发 HTTP(约 3 RTT)
TLS 1.3:TCP 三次握手 → TLS 一次往返 → 发 HTTP(约 2 RTT)
0-RTT:复用上次会话参数,首个请求即可带数据(但有重放风险)
0-RTT 的风险:早期数据可能被重放,只能用于幂等请求(GET),不能用于支付、下单。
3.2 连接复用与「排队」
浏览器对同一域名:
HTTP/1.1 → 约 6 条连接,超出排队
HTTP/2 → 1 条连接多路复用,但受 TCP 窗口与优先级影响
判断方法:Network 瀑布图里,如果多个请求的 Queueing 很长、Connection Start 有 Stalled,就是连接不足或代理限制。
3.3 资源优先级与 Early Hints
HTTP/1.1 103 Early Hints
Link: </app.css>; rel=preload; as=style
Link: </hero.webp>; rel=preload; as=image
服务端在最终响应前先告诉浏览器「接下来要这些资源」,浏览器可以提前下载。对「服务端渲染慢」的站点收益明显。
3.4 WebSocket / SSE / WebTransport
| 方案 | 方向 | 协议 | 适合 | 限制 |
|---|---|---|---|---|
| 轮询 | 双向(伪) | HTTP | 低频、简单 | 延迟与浪费 |
| 长轮询 | 双向(伪) | HTTP | 兼容性要求高 | 连接占用 |
| SSE | 服务端 → 客户端 | HTTP | 通知、日志、AI 流式输出 | 单向、HTTP/1.1 下有连接数限制 |
| WebSocket | 双向 | WS | 聊天、协作、游戏 | 需要心跳、重连、代理支持 |
| WebTransport | 双向 | HTTP/3 | 低延迟、多流 | 生态与兼容性尚早 |
/**
* SSE:最适合「服务端持续推送、客户端不常发」的场景。
* 相比 WebSocket,它自带断线重连与事件 id,实现成本低。
*/
const es = new EventSource('/api/notifications');
es.onmessage = (e) => append(JSON.parse(e.data));
es.onerror = () => {
// 浏览器会自动重连;如需退避,可关闭后手动重建
};
/**
* WebSocket:必须自己做心跳与重连。
*/
function connect(url) {
const ws = new WebSocket(url);
let timer;
ws.onopen = () => {
timer = setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 30_000);
};
ws.onclose = () => {
clearInterval(timer);
setTimeout(() => connect(url), Math.min(30_000, retry * 2)); // 指数退避
};
return ws;
}
3.5 压缩与传输优化
| 内容 | 推荐 | 说明 |
|---|---|---|
| 文本(JS/CSS/HTML/JSON) | Brotli(br) | 比 gzip 再小 15%–20% |
| 图片 | WebP/AVIF | 按浏览器能力降级 |
| 视频 | H.264/AV1 + Range | 分片与码率自适应 |
| 已压缩格式(png/zip/mp4) | 不再压缩 | 浪费 CPU |
Accept-Encoding: gzip, deflate, br
Content-Encoding: br
Vary: Accept-Encoding
Vary 缺失的后果:CDN 可能把 br 版本返回给不支持 br 的客户端,导致乱码。
3.6 断点续传与 Range
GET /video.mp4
Range: bytes=0-1048575
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/10485760
Accept-Ranges: bytes
用于大文件下载、视频拖动、失败重试只补缺失部分。
3.7 DNS 与连接预热
<!-- 提前解析域名(不建立连接) -->
<link rel="dns-prefetch" href="//api.example.com" />
<!-- 提前建连 + TLS(省握手) -->
<link rel="preconnect" href="https://api.example.com" crossorigin />
成本:preconnect 会占用连接资源,只对「确定马上要用」的域名使用,控制在 2–4 个。
3.8 代理与网关带来的「隐形延迟」
| 环节 | 常见问题 | 观察点 |
|---|---|---|
| Nginx | proxy_buffering 缓冲流式响应 | SSE 不实时 |
| 网关 | 默认超时 30s,长请求被断 | 504 |
| 负载均衡 | 会话未粘滞,登录态漂移 | 偶发 401 |
| 服务网格 | 重试放大流量 | 雪崩 |
SSE/流式场景必配:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
3.9 网络排查:把一次请求拆成时间片
DevTools 的 Timing 面板把请求拆成若干段,每段对应不同的责任方:
| 阶段 | 含义 | 高值通常说明 |
|---|---|---|
| Queueing | 排队等待 | 连接数打满、优先级低 |
| Stalled | 等待可用连接 | 同域请求过多、HTTP/1.1 限制 |
| DNS Lookup | 域名解析 | DNS 慢、无预解析 |
| Initial connection | TCP 握手 | 跨地域、丢包 |
| SSL | TLS 握手 | 证书链长、未启用 TLS 1.3 |
| Request sent | 发送请求 | 通常很小 |
| Waiting (TTFB) | 等首字节 | 服务端处理慢、回源慢 |
| Content Download | 下载响应 | 体积大、带宽低、未压缩 |
/**
* 用 Resource Timing 采集分段耗时,定位慢在「连接」还是「服务端」。
* @param {string} url 资源地址
*/
function getTiming(url) {
const entry = performance.getEntriesByName(url).at(-1);
if (!entry) return null;
return {
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
tls: entry.secureConnectionStart
? entry.connectEnd - entry.secureConnectionStart
: 0,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart,
total: entry.duration,
};
}
四类典型现象与判断:
| 现象 | 优先怀疑 | 验证方式 |
|---|---|---|
大量请求 Stalled | HTTP/1.1 连接上限、同域资源过多 | 切 HTTP/2 或分域,观察是否消失 |
| TTFB 高但下载快 | 服务端/网关/回源 | 看服务端耗时与 CDN X-Cache |
| TTFB 正常但下载慢 | 体积、带宽、压缩 | 看 Content-Encoding 与 gzip 后大小 |
| 首包快、后续慢 | 流式输出被缓冲 | 检查代理缓冲与 Transfer-Encoding |
工具:DevTools Network(单请求)、Performance(整体瀑布)、HAR(可离线分析)、chrome://net-export(连接层)、WebPageTest(多地域多网络)。
一个常被忽略的点:测试环境的「本机 → 同城服务器」几乎无法复现真实网络问题;必须用限速、跨地域节点或真实用户监控(RUM)。
3.10 L3 小结与自检
| 主题 | 关键结论 |
|---|---|
| TLS 1.3 | 1-RTT,0-RTT 只用于幂等请求 |
| 连接 | HTTP/2 单连接多路复用,仍受 TCP 影响 |
| 推送 | Early Hints 可提前发现资源 |
| 实时 | 通知用 SSE,双向用 WebSocket |
| 压缩 | 文本用 br,已压缩格式别再压 |
| 网关 | 流式响应必须关缓冲 |
自检题
- 为什么 SSE 在 HTTP/1.1 下容易「卡住」?怎么解决?——同域 6 个连接上限被长连接占满,其他请求只能排队;改用 HTTP/2 多路复用或独立域名,并加心跳防止代理断开。
- TLS 0-RTT 为什么不能用于下单请求?——0-RTT 数据可被重放,非幂等操作会重复执行;只应用于幂等的 GET 类请求。
- 视频拖动进度条用到了哪个 HTTP 能力?——
Range请求头加206 Partial Content,服务端声明Accept-Ranges: bytes。 - 请求
Stalled很久,通常是前端还是服务端问题?——多半是浏览器连接调度:同域连接被占满、优先级低或 HTTP/1.1 并发限制;应查请求瀑布与协议版本,而不是先查后端。
4. L4 专家:缓存体系、容灾与成本
4.1 一套可解释的缓存体系
┌──────────────┐ 未命中 ┌──────────────┐ 未命中 ┌──────────────┐
│ 浏览器缓存 │ ───────▶ │ CDN 边缘缓存 │ ───────▶ │ 源站/服务端 │
│ 强缓存+协商 │ │ TTL + purge │ │ 数据缓存/DB │
└──────────────┘ └──────────────┘ └──────────────┘
| 层 | 缓存对象 | 失效策略 | 关键指标 |
|---|---|---|---|
| 浏览器 | 静态资源、HTML | hash、no-cache | 命中率 |
| CDN | HTML、图片、JS | TTL、purge、预热 | 回源率 |
| 服务端 | 接口结果 | key 版本、TTL、主动失效 | 缓存命中率 |
| 数据库 | 查询结果 | 事务、TTL | 慢查询比例 |
设计原则:
- 按变化频率分层:越稳定的越往前放;
- 按个性化隔离:私有数据不进共享层;
- 每一层都要能失效:没有失效手段的缓存是定时炸弹;
- 缓存键要完整:语言、设备、灰度分组都要进 key,否则会串内容。
4.2 多地域与容灾
| 策略 | 做法 | 代价 |
|---|---|---|
| 多活 | 多地同时提供服务,DNS/Anycast 调度 | 数据一致性复杂 |
| 主备 | 主机房故障切备 | 切换时长、数据回追 |
| 边缘计算 | 逻辑下沉到边缘 | 状态与调试复杂 |
| 降级 | 接口失败返回兜底数据 | 需要产品接受 |
前端能做的容灾:
/**
* 接口兜底:失败时用缓存或默认值,避免整页白屏。
*/
async function loadWithFallback(key, fetcher, fallback) {
try {
const data = await fetcher();
localStorage.setItem(key, JSON.stringify(data));
return data;
} catch {
const cached = localStorage.getItem(key);
return cached ? JSON.parse(cached) : fallback;
}
}
4.3 安全相关的协议配置
| 配置 | 作用 | 建议 |
|---|---|---|
| HSTS | 强制 HTTPS | max-age=31536000; includeSubDomains |
SameSite | 限制跨站携带 Cookie | 默认 Lax,敏感操作 Strict |
HttpOnly | 禁止 JS 读取 | 会话 Cookie 必加 |
Secure | 仅 HTTPS 发送 | 必加 |
| CORS 白名单 | 限制来源 | 禁止反射任意 Origin |
| 预检缓存 | 减少 OPTIONS | Max-Age 合理设置 |
第三方 Cookie 逐步退场:依赖三方 Cookie 的埋点、SSO、广告会失效,需要迁移到第一方域名或服务端转发。
4.4 度量:怎么证明网络优化有效
| 指标 | 定义 | 目标参考 |
|---|---|---|
| TTFB | 首字节 | < 800ms(P75) |
| DNS/连接耗时 | 建连阶段 | < 100ms(有预热) |
| 缓存命中率 | CDN/浏览器 | > 80% |
| 回源率 | CDN 回源比例 | < 20% |
| 错误率 | 5xx/网络失败 | < 0.1% |
| 传输体积 | 单页总字节 | 按业务定预算 |
方法:RUM 采集真实用户数据 → 按地域/运营商/页面拆分 → 找到长尾 → 针对性优化(预热、就近接入、协议升级)。
4.5 成本视角
| 成本 | 影响因素 | 优化手段 |
|---|---|---|
| CDN 流量 | 体积 × 请求数 | 压缩、图片格式、缓存命中 |
| 回源带宽 | 回源率 | 提高 TTL、预热、合并请求 |
| 服务端 | QPS × 耗时 | 缓存、聚合、限流 |
| 跨地域传输 | 距离与数据量 | 边缘聚合、字段裁剪 |
判断标准:每 1% 的缓存命中率提升,能省多少回源成本?没有这个数字,优化就没有优先级。
4.6 L4 自检
- 一个全球访问的产品,如何设计 DNS、CDN、源站的分层?——GeoDNS/Anycast 就近接入,CDN 边缘缓存静态与可缓存 HTML,区域源站多活;动态请求走最优回源与连接复用,并准备容灾切换。
- 缓存命中率从 70% 提到 90%,怎么证明收益?——换算回源请求数与带宽、源站成本、TTFB 与 p75 的变化;用同期对照与分地域数据,给出成本节省与体验收益。
- 三方 Cookie 退场后,现有 SSO 与埋点怎么迁移?——SSO 改第一方 Cookie 加重定向,或使用 CHIPS/Storage Access;埋点转第一方域或服务端上报,建立统一 ID 并评估覆盖率损失。
5. 综合实战:接口 P99 800ms 的排查与治理
背景:列表接口 P75 120ms,P99 800ms,用户偶发「转圈很久」。
步骤 1 · 分段测量
DNS 0ms(已缓存)
连接 0ms(复用)
TLS 0ms
TTFB 780ms ← 瓶颈在这里
下载 20ms
结论:不是前端、不是传输,是服务端处理或排队。
步骤 2 · 定位服务端
| 假设 | 验证 | 结果 |
|---|---|---|
| 慢查询 | 看 DB 慢日志 | 命中,某查询在数据量大时全表扫 |
| 连接池耗尽 | 看等待队列 | 高峰期排队 |
| 缓存击穿 | 看缓存命中率 | 大 key 过期瞬间回源 |
| 网关超时重试 | 看上游 QPS 翻倍 | 存在重试放大 |
步骤 3 · 治理
/**
* 前端侧能做的三件事:
* 1) 请求去重(同一 key 并发只发一次)
* 2) 缓存兜底(stale-while-revalidate 思路)
* 3) 超时与退避(避免用户无限等待)
*/
const inflight = new Map();
function getJSON(url, { timeout = 8000 } = {}) {
if (inflight.has(url)) return inflight.get(url);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const p = fetch(url, { signal: controller.signal })
.then((r) => {
if (!r.ok) throw new Error(String(r.status));
return r.json();
})
.finally(() => {
clearTimeout(timer);
inflight.delete(url);
});
inflight.set(url, p);
return p;
}
服务端侧:加索引、缓存热点、连接池调优、限制重试次数。
步骤 4 · 验收
- P99 < 300ms
- 缓存命中率 > 85%
- 超时请求有明确兜底 UI
- 重试不会造成流量放大(有上限与退避)
- 关键接口有 SLO 与告警
6. 常见误区对照表
| 误区 | 事实 |
|---|---|
| 「跨域是后端没配好」 | 是浏览器策略;后端配的是「允许谁读」 |
「no-cache 是不缓存」 | 是「缓存但每次验证」;不缓存是 no-store |
| 「304 很快」 | 省带宽不省往返,仍要看 TTFB |
| 「HTTP/2 就不用拆包了」 | 拆包仍利于缓存粒度与按需加载 |
| 「JWT 比 Session 安全」 | 各有风险;JWT 难吊销,Cookie 需防 CSRF |
| 「CDN 缓存越久越好」 | 发布不生效、个性化串号都是这么来的 |
| 「preconnect 越多越好」 | 占连接与 CPU,2–4 个足够 |
| 「压缩一定更快」 | 已压缩格式再压是浪费 CPU |
| 「WebSocket 适合所有实时场景」 | 单向推送用 SSE 更简单可靠 |
| 「0-RTT 全面提速」 | 有重放风险,只能用于幂等请求 |
7. 面试高频题(附答题思路)
Q1:从输入 URL 到收到响应,网络层发生了什么? DNS → TCP → TLS → HTTP 请求 → 服务端 → 响应 → 渲染。每段说一个可优化的点(缓存、preconnect、HTTP/2、CDN)。
Q2:强缓存与协商缓存的区别?
是否发请求;Cache-Control/Expires vs ETag/Last-Modified;304 的收益与局限;给资源类型配比。
Q3:CORS 预检什么时候触发?怎么减少?
非简单请求(方法/头/类型);Max-Age 缓存预检;同域或 BFF 规避。
Q4:HTTP/1.1、2、3 的差异? 队头阻塞层次不同;多路复用;QUIC 与 0-RTT;对构建策略的影响。
Q5:Cookie、localStorage、JWT 怎么选? 按威胁模型:XSS vs CSRF;会话撤销需求;跨域场景;给组合方案。
Q6:如何设计一套缓存策略? 按资源变化频率分层;hash 文件名 + 长缓存;HTML 短缓存;私有数据隔离;失效与预热;度量命中率。
Q7:SSE 与 WebSocket 怎么选? 方向、协议开销、重连、代理兼容;AI 流式输出首选 SSE。
Q8:接口慢怎么排查? Network 分段 → 区分 DNS/连接/TTFB/下载 → 服务端指标 → 缓存与重试放大 → 前端去重与兜底。
8. 面试深挖
8.1 面试官在考察什么
| 维度 | 看什么 | 加分回答 | 减分回答 |
|---|---|---|---|
| 协议演进 | 是否理解「队头阻塞」在不同层的含义 | 区分应用层与传输层 HOL | 「HTTP/2 解决了所有问题」 |
| 缓存 | 能否解释一次命中/未命中的完整判定 | 说出缓存键、Vary、失效顺序 | 「加 max-age 就行」 |
| 跨域 | 是否知道请求发没发出去 | 能按预检、凭证、响应头顺序排错 | 「后端加 * 就行」 |
| 鉴权 | 存储位置与攻击面的关系 | 说清 XSS 与 CSRF 的不同防线 | 「token 放 localStorage 方便」 |
| 排错 | 会不会读时序 | 用 Timing 面板与 Server-Timing 定位 | 只会看瀑布图截图 |
8.2 追问链
链 1 · HTTP/1.1 → HTTP/2 → HTTP/3
- 面试官:HTTP/1.1 的队头阻塞是什么?
- 要点:同一连接上请求串行,前一个响应不完整会阻塞后续响应;浏览器用 6 个并发连接缓解,但带来连接开销与优先级竞争。
- 追问:HTTP/2 的多路复用解决了什么,又留下了什么?
- 要点:用二进制分帧把请求拆成 stream,在同一 TCP 连接上并发交错传输,解决应用层 HOL;但 TCP 层仍要求按序交付,丢一个包会阻塞该连接上所有 stream,这就是 TCP 层 HOL。
- 再追问:QUIC 怎么解决,代价是什么?
- 要点:基于 UDP 在用户态实现可靠传输,stream 独立重传,丢包只影响所在 stream;0-RTT 可复用会话,但 0-RTT 数据有重放风险,只能用于幂等请求。
链 2 · 强缓存 → 协商缓存 → 缓存键
- 面试官:浏览器什么时候直接用缓存?
- 要点:先看
Cache-Control(max-age、s-maxage、no-store、no-cache、must-revalidate),新鲜则直接用;no-cache不是不缓存,而是每次必须校验;过期后带If-None-Match/If-Modified-Since走协商,命中返回 304。
- 要点:先看
- 追问:
ETag和Last-Modified谁的优先级高?- 要点:
ETag更精确(内容哈希或版本),服务端应优先比对;Last-Modified精度只到秒,且文件时间可能被部署工具改动。
- 要点:
- 再追问:CDN 上为什么会出现「同 URL 不同内容」?
- 要点:缓存键没包含
Vary指定的头(如Accept-Encoding、Accept-Language),或未按租户/设备分键;也可能是回源时命中不同源站。排查要看响应头Age、X-Cache、Vary。
- 要点:缓存键没包含
链 3 · 简单请求 → 预检 → 凭证
- 面试官:什么情况会触发预检?
- 要点:方法不是
GET/HEAD/POST,或Content-Type不是三者之一,或带有自定义请求头;预检是OPTIONS,需要服务端返回Access-Control-Allow-*且包含实际请求的方法与头。
- 要点:方法不是
- 追问:预检结果会缓存吗?
- 要点:会,由
Access-Control-Max-Age控制(各浏览器有上限,Chrome 约 2 小时);缓存键包含 origin、方法、头,任一变化都会重新预检。
- 要点:会,由
- 再追问:带 Cookie 的跨域要注意什么?
- 要点:前端
credentials: 'include',服务端Access-Control-Allow-Credentials: true且Allow-Origin不能是*;Cookie 需SameSite=None; Secure。任一处缺失,表现都是「请求发了但没有 Cookie」或「响应被拦」。
- 要点:前端
链 4 · CDN 缓存 → 回源 → 一致性
- 面试官:发布后用户仍拿到旧页面,怎么排查?
- 要点:先看 HTML 的缓存策略(是否
no-cache/短 TTL),再看 CDN 是否执行了 purge、边缘节点是否全部生效;最后确认浏览器缓存与服务端模板版本。
- 要点:先看 HTML 的缓存策略(是否
- 追问:为什么静态资源可以长缓存,HTML 不行?
- 要点:静态资源带内容 hash,内容变则 URL 变,可设一年;HTML 是入口,必须尽快指向新 hash,通常用短 TTL + 协商或主动刷新。
- 再追问:
stale-while-revalidate会不会让用户看到旧数据?- 要点:会,但只在一个短窗口内,且后台会更新;对价格、库存等强一致数据要禁用或配合版本号。
链 5 · 鉴权 → 并发刷新 → 失败处理
- 面试官:多个请求同时 401,怎么保证只刷新一次?
- 要点:用单飞(single-flight)锁:第一个请求发起刷新,其余等待同一个 Promise;刷新成功后统一重放,失败则统一登出。
- 追问:刷新令牌轮换后,旧令牌还能用吗?
- 要点:应失效并检测重放;服务端给每个刷新令牌家族记录版本,发现旧令牌被使用则吊销整个家族。
- 再追问:重放请求要注意什么?
- 要点:只重放幂等请求;非幂等请求要由业务层确认是否已执行,避免重复下单;设置最大重试次数与超时。
8.3 手写题
题 1 · 带超时、重试与取消的请求封装
/**
* 请求封装:超时、指数退避重试、外部取消。
* @param {string} url
* @param {RequestInit & {timeout?: number, retries?: number, signal?: AbortSignal}} [options]
* @returns {Promise<Response>}
*/
async function request(url, { timeout = 8000, retries = 2, signal, ...init } = {}) {
/** 单次请求:超时与外部取消合并 */
const once = () => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new DOMException('timeout', 'TimeoutError')), timeout);
const onAbort = () => controller.abort(signal.reason);
signal?.addEventListener('abort', onAbort, { once: true });
return fetch(url, { ...init, signal: controller.signal }).finally(() => {
clearTimeout(timer);
signal?.removeEventListener('abort', onAbort);
});
};
for (let attempt = 0; ; attempt += 1) {
try {
const res = await once();
// 5xx 可重试,4xx 不重试
if (res.status >= 500 && attempt < retries) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
const aborted = signal?.aborted || err.name === 'AbortError';
if (aborted || attempt >= retries) throw err;
// 指数退避 + 抖动,避免重试风暴
const delay = 200 * 2 ** attempt + Math.random() * 100;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
追问:
- 为什么超时要单独建
AbortController?→ 外部取消与超时是两种语义,合并后无法区分;也可用AbortSignal.any。 - 什么请求不能重试?→ 非幂等的
POST(下单、支付),除非有幂等键;重试前应判断方法与业务语义。 - 重试抖动有什么用?→ 避免大量客户端在同一时刻重试造成雪崩。
题 2 · SSE 客户端
/**
* SSE 客户端:解析 event/data/id,支持断线重连与 Last-Event-ID。
* @param {string} url
* @param {{onMessage: (data: string, event: string) => void, retry?: number}} handlers
*/
function createSSE(url, { onMessage, retry = 3000 }) {
let controller = null;
let lastId = '';
let stopped = false;
const connect = async () => {
controller = new AbortController();
const res = await fetch(url, {
headers: lastId ? { 'Last-Event-ID': lastId } : {},
signal: controller.signal,
});
if (!res.ok || !res.body) throw new Error(`SSE ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 事件以空行分隔,\r\n 与 \n 都要兼容
const blocks = buffer.split(/\r?\n\r?\n/);
buffer = blocks.pop() ?? '';
for (const block of blocks) {
let event = 'message';
const data = [];
block.split(/\r?\n/).forEach((line) => {
if (line.startsWith('event:')) event = line.slice(6).trim();
else if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
else if (line.startsWith('id:')) lastId = line.slice(3).trim();
});
if (data.length) onMessage(data.join('\n'), event);
}
}
};
const start = async () => {
stopped = false;
while (!stopped) {
try {
await connect();
} catch (err) {
if (stopped) return;
}
await new Promise((resolve) => setTimeout(resolve, retry));
}
};
return {
start,
close() {
stopped = true;
controller?.abort();
},
};
}
追问:
- 为什么要自己解析而不用
EventSource?→ 需要自定义请求头(鉴权)、POST、或读取非标准字段;代价是要自己处理重连与分块边界。 - 一个中文被拆在两个 chunk 里怎么办?→
TextDecoder用{ stream: true }保留半个字符的字节。 - 断线重连怎么避免重复消息?→ 服务端按
Last-Event-ID续传,客户端按id去重。
8.4 场景设计题
题:一个跨域的后台系统要改造登录态,同时支持 Web 与 App 内嵌页,还要传大文件。你怎么设计?
评估点:是否按端与攻击面分方案、是否考虑大文件与断点。
参考方案:
- Web 端:Cookie +
HttpOnly; Secure; SameSite=Lax(跨站场景用None; Secure),配合 CSRF token(双提交或同步令牌);BFF 统一注入,前端不碰凭证。 - App 内嵌:由容器注入短期 token,走 Authorization 头;token 存内存 + 安全存储,不写
localStorage;刷新用静默续期。 - 跨域:精确配置
Allow-Origin白名单与Allow-Credentials;预检缓存设Max-Age;错误统一走 401 刷新、403 提示。 - 大文件:分片(如 5MB)+ 并发 3 路 + 断点续传(记录已完成分片);秒传用文件哈希;上传走独立域名避免占用主域连接。
- 验证:401 比例、续期成功率、上传成功率与平均耗时、跨域错误率。
加分:指出 token 放 localStorage 会被 XSS 直接读取,Cookie 则要防 CSRF,两者是不同防线的取舍;指出分片大小要按网络自适应。
减分:用 * 配 Allow-Credentials;只答「加 token」不谈存储与刷新;大文件直传不做分片。
题 2:核心列表接口 P99 800ms、P50 120ms,CDN 命中率只有 20%,前端已经做了骨架屏但用户仍抱怨慢。你怎么定位并治理?
评估点:是否区分「单次慢」与「长尾慢」、是否从链路分段与缓存两层下手。
参考方案:
- 拆口径:先看分布:P50 正常说明多数请求没问题,P99 高说明是长尾(回源、跨地域、慢查询、重试)。按地域、运营商、版本、是否登录分桶。
- 分段:用 Resource Timing / 服务端 trace 拆 DNS、连接、TTFB、下载;TTFB 高则查服务端与回源,下载慢则查体积与压缩。
- 缓存:检查缓存键是否包含随机参数、
Vary是否过宽、CDN 是否按用户维度分键导致命中率低;把公共部分做成可缓存接口,个性化字段单独请求。 - 前端体验:骨架屏只能改善感知,不能减少等待;对首屏必需数据做并行请求与预取,对非关键数据延后加载。
- 验收:P99 降到 300ms 内、命中率提升到 70%+、错误率与超时率下降;用灰度对比确认不是流量结构变化带来的假象。
加分:指出「P50 好、P99 差」通常是长尾基础设施问题(跨地域、连接复用、慢查询),不是前端渲染问题;指出缓存命中率要按 URL 规范化后统计。
减分:只加骨架屏;只扩容不考虑缓存键;用平均值汇报。
8.5 最容易答错的点
| 常见说法 | 纠正 |
|---|---|
| 「HTTP/2 没有队头阻塞」 | 应用层没有,TCP 丢包仍会阻塞整条连接 |
| 「HTTP/3 一定比 HTTP/2 快」 | 弱网收益明显,内网/低丢包环境差异很小 |
「no-cache 是不缓存」 | 是「每次使用前必须校验」,no-store 才是不存 |
| 「304 没有响应体所以不耗流量」 | 省的是 body,请求与响应头仍要往返 |
| 「CORS 是服务端拦截请求」 | 是浏览器拦截响应读取,请求可能已到达服务端 |
「SameSite=Lax 能防所有 CSRF」 | 只限制跨站携带,站内 XSS 与同站子域仍可发起 |
「localStorage 存 token 更安全」 | 任何 XSS 都能读取,HttpOnly Cookie 才隔离 JS |
「加 Cache-Control: max-age 就一定有缓存」 | 还受 Vary、缓存键、CDN 配置与刷新策略影响 |
| 「P50 正常就说明接口没问题」 | 长尾用户可能极慢,必须看 P95/P99 与分桶 |
| 「骨架屏能解决慢的问题」 | 它只改善感知,等待时间与失败率不变 |
| 「CDN 命中率低就是 CDN 不行」 | 多半是缓存键含随机参数、Vary 过宽或个性化分键 |
| 「token 刷新失败就重试」 | 会触发请求风暴,应限制次数并统一登出 |
9. 延伸资源
- MDN:HTTP 缓存、CORS、Cookie 属性、HTTP/2、HTTP/3
- RFC 9111(HTTP 缓存)、RFC 9110(HTTP 语义)、RFC 9000(QUIC)
- web.dev:Cache-Control、preconnect、Early Hints、HTTP/3
- High Performance Browser Networking(O'Reilly,免费在线版)
- Cloudflare / Fastly 博客:CDN 缓存键与回源优化
- OWASP:Session Management、CSRF、CORS 配置指南
- OAuth 2.0 Security Best Current Practice(含 PKCE)
- Chrome 团队:第三方 Cookie 退场与 Privacy Sandbox 文档