网络协议与浏览器通信

中等 🟡浏览器原理
9 个标签
预计阅读时间:64 分钟
浏览器原理网络协议HTTPHTTPSWebSocketTCP/IPTLSCORS性能优化

网络协议与浏览器通信

网络协议是浏览器与服务器通信的基础。从你在地址栏敲下回车,到页面第一个像素显示出来,中间要经历 DNS 解析、TCP 连接、TLS 握手、HTTP 请求响应、浏览器渲染等一整套流程。理解这些环节,既能帮你排查"为什么这个接口这么慢",也能帮你在面试中把"输入 URL 到页面展示"这道经典题讲清楚。

打个比方:浏览器请求一个网页,就像你要给一个陌生人寄一封重要的信——

DNS 解析:先要查到对方的门牌号(IP 地址)
TCP 三次握手:打电话确认"喂,你在吗?""在,你说""好的,我开始寄了"
TLS 握手:约定一个只有你俩懂的暗号(加密方式),防止信被人半路拆开偷看
HTTP 请求响应:正式把信(数据)寄过去,对方收到后回信
TCP 四次挥手:礼貌地互相确认"我们聊完了""好的,挂了"

下面按照这个流程逐层展开,每个环节都配代码示例、对比表格和常见坑。

🗺️ 完整网络请求流程总览

在浏览器地址栏输入 \`https://www.example.com\` 并回车后,大致会发生:

1.URL 解析:浏览器判断这是一个合法 URL 还是搜索关键词,解析出协议、域名、端口、路径
2.DNS 解析:将域名解析为 IP 地址(可能命中浏览器缓存、系统缓存、路由器缓存,否则逐级查询 DNS 服务器)
3.建立 TCP 连接:与目标 IP 的对应端口(HTTPS 默认 443)进行三次握手
4.TLS 握手(如果是 HTTPS):协商加密算法、交换密钥、验证证书
5.发送 HTTP 请求:浏览器组装请求行、请求头、请求体,通过已建立的连接发出
6.服务器处理请求:服务器路由、鉴权、查询数据库、渲染模板或返回 JSON
7.浏览器接收响应:解析状态码、响应头,决定如何处理响应体
8.渲染页面:如果是 HTML,浏览器开始构建 DOM/CSSOM、生成渲染树、布局、绘制、合成
9.后续资源加载:解析过程中发现的 CSS、JS、图片等资源,会触发新的请求(可能复用已有连接)
10.TCP 四次挥手:连接不再需要时关闭(HTTP/1.1 的 Keep-Alive、HTTP/2 的连接复用会让这一步延后发生)

下面这张表概括了每个阶段大致的耗时量级(数值会因网络环境剧烈波动,仅作直观参考):

| 阶段 | 典型耗时(无缓存、跨运营商) | 说明 |

| --- | --- | --- |

| DNS 解析 | 20ms ~ 120ms | 命中缓存可低至 0ms |

| TCP 三次握手 | 1 个 RTT(约 20ms ~ 100ms) | 受物理距离影响很大 |

| TLS 握手 | TLS1.2 为 2 个 RTT,TLS1.3 为 1 个 RTT(甚至 0-RTT) | 详见下文 TLS 章节 |

| TTFB(首字节时间) | 100ms ~ 600ms | 包含服务器处理时间 |

| 内容下载 | 视资源大小和带宽而定 | 可通过压缩、CDN 优化 |

| 渲染 | 与网络无关,取决于页面复杂度 | 见《浏览器架构与渲染流程》 |

🔎 DNS 解析详解

DNS(Domain Name System)负责把人类容易记忆的域名转换成机器能理解的 IP 地址,本质上是一个分布式的"域名电话簿"。

查询过程(假设完全没有缓存):

1.浏览器检查自身的 DNS 缓存
2.检查操作系统的 DNS 缓存(如 hosts 文件、系统解析器缓存)
3.检查路由器缓存
4.向 ISP 的递归 DNS 服务器发起查询
5.递归服务器依次查询根域名服务器顶级域名服务器(.com)权威域名服务器(example.com)
6.权威服务器返回 A 记录(IPv4)或 AAAA 记录(IPv6)
7.递归服务器把结果缓存下来并返回给浏览器

常见 DNS 记录类型:

| 记录类型 | 作用 |

| --- | --- |

| A | 域名指向 IPv4 地址 |

| AAAA | 域名指向 IPv6 地址 |

| CNAME | 别名记录,指向另一个域名 |

| MX | 邮件服务器记录 |

| TXT | 文本记录,常用于域名验证、SPF 反垃圾邮件配置 |

| NS | 指定该域名由哪些域名服务器解析 |

优化 DNS 的手段:

减少页面依赖的第三方域名数量(每多一个域名就多一次 DNS 查询)
使用 \`\` 提前解析后续会用到的域名
选择解析速度快、就近节点多的 DNS 服务商(如 Cloudflare 的 1.1.1.1)
合理设置 DNS TTL,权衡"更新生效速度"与"缓存命中率"

🤝 TCP 连接:三次握手与四次挥手

HTTP 建立在 TCP 之上(HTTP/3 除外,它基于 QUIC/UDP),TCP 是一个面向连接、可靠的传输层协议。"面向连接"意味着通信双方要先协商好,才能正式发数据,这就是三次握手的由来。

三次握手(建立连接):

\`\`\`text

客户端 服务器

|--- SYN (seq=x) --------------------->| "我想和你建立连接,我的序号从 x 开始"

|<-- SYN-ACK (seq=y, ack=x+1) ----------| "收到,我同意,我的序号从 y 开始"

|--- ACK (ack=y+1) --------------------->| "收到,连接建立成功"

\`\`\`

为什么是三次而不是两次?因为服务器发出 SYN-ACK 后,还需要客户端确认自己的 ACK 已经被服务器收到,否则会出现"服务器以为连接建立了,客户端却不知道"的半开连接,浪费服务器资源。这也是为什么可以用类比理解:

客户端:"喂,你能听到我说话吗?"(SYN)
服务器:"我能听到,你能听到我吗?"(SYN-ACK)
客户端:"我也能听到,那我们开始通话吧"(ACK)

四次挥手(关闭连接):

\`\`\`text

客户端 服务器

|--- FIN (seq=u) ----------------------->| "我这边数据发完了,准备关闭"

|<-- ACK (ack=u+1) ----------------------| "收到,稍等,我这边可能还有数据要发"

|<-- FIN (seq=v) -------------------------| "我这边也发完了,可以关了"

|--- ACK (ack=v+1) ----------------------->| "收到,关闭"

\`\`\`

四次挥手比三次握手多一次,是因为 TCP 是全双工的:客户端说"我说完了"之后,服务器可能还有数据没发完,所以服务器的 ACK 和 FIN 通常是分开发送的(也存在合并成三次挥手的情况,取决于是否有数据待发送)。

拥塞控制与流量控制的区别:

| 机制 | 解决的问题 | 核心手段 |

| --- | --- | --- |

| 流量控制 | 防止发送方发送速度超过接收方处理能力 | 滑动窗口(接收方通告窗口大小) |

| 拥塞控制 | 防止网络整体过载 | 慢启动、拥塞避免、快重传、快恢复 |

慢启动简述:连接刚建立时,发送方并不知道网络能承受多大流量,于是从一个很小的拥塞窗口开始,每收到一个 ACK 就成倍增大窗口(指数增长),直到达到阈值后转为线性增长(拥塞避免),一旦检测到丢包就大幅减小窗口,这也是为什么"新建连接的前几个请求往往比复用连接的请求慢"的原因之一——这正是 HTTP/2、HTTP/3 强调"复用连接、减少连接数"的重要原因。

📨 HTTP 请求与响应结构

一个完整的 HTTP 请求由三部分组成:请求行请求头请求体(GET 请求通常没有请求体)。

\`\`\`text

GET /api/users?id=123 HTTP/1.1

Host: api.example.com

User-Agent: Mozilla/5.0

Accept: application/json

Authorization: Bearer xxx.yyy.zzz

Cache-Control: no-cache

(GET 请求通常无请求体)

\`\`\`

响应同样由状态行响应头响应体组成:

\`\`\`text

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

Content-Length: 128

Cache-Control: max-age=3600

ETag: "33a64df551"

{"id":123,"name":"张三"}

\`\`\`

常见请求方法:

| 方法 | 用途 | 是否幂等 | 是否有请求体 |

| --- | --- | --- | --- |

| GET | 获取资源 | 是 | 通常没有 |

| POST | 创建资源/提交数据 | 否 | 有 |

| PUT | 完整替换资源 | 是 | 有 |

| PATCH | 局部更新资源 | 否(约定上通常也做成幂等) | 有 |

| DELETE | 删除资源 | 是 | 通常没有 |

| HEAD | 只获取响应头,不要响应体 | 是 | 没有 |

| OPTIONS | 询问服务器支持的方法(CORS 预检核心) | 是 | 没有 |

常见状态码:

| 状态码 | 类别 | 含义 | 常见场景 |

| --- | --- | --- | --- |

| 200 | 成功 | 请求成功 | 正常返回数据 |

| 201 | 成功 | 已创建 | POST 创建资源成功 |

| 204 | 成功 | 无内容 | 删除成功、OPTIONS 预检响应 |

| 301 | 重定向 | 永久重定向 | 域名迁移、强制 HTTPS |

| 302 | 重定向 | 临时重定向 | 登录后跳转 |

| 304 | 重定向 | 未修改,使用协商缓存 | ETag/Last-Modified 验证命中 |

| 400 | 客户端错误 | 请求参数错误 | 参数校验失败 |

| 401 | 客户端错误 | 未认证 | 未登录或 token 失效 |

| 403 | 客户端错误 | 无权限 | 已登录但权限不足 |

| 404 | 客户端错误 | 资源不存在 | 路径错误 |

| 429 | 客户端错误 | 请求过于频繁 | 触发限流 |

| 500 | 服务端错误 | 服务器内部错误 | 代码异常、未捕获的报错 |

| 502 | 服务端错误 | 网关错误 | 上游服务无响应 |

| 503 | 服务端错误 | 服务不可用 | 服务器过载或维护中 |

| 504 | 服务端错误 | 网关超时 | 上游服务处理超时 |

代码示例:fetch 完整用法(超时、重试、错误处理)

真实项目里很少直接裸用 \`fetch\`,通常需要封装超时取消、失败重试、统一错误处理:

\`\`\`javascript

async function fetchWithRetry(url, options = {}, retries = 3, timeout = 5000) {

const controller = new AbortController();

const timer = setTimeout(() => controller.abort(), timeout);

try {

const response = await fetch(url, { ...options, signal: controller.signal });

clearTimeout(timer);

if (!response.ok) {

// 4xx/5xx 不会让 fetch 走 catch 分支,需要手动判断

const errorBody = await response.text().catch(() => '');

throw new Error(\`HTTP ${response.status}: ${errorBody || response.statusText}\`);

}

return await response.json();

} catch (error) {

clearTimeout(timer);

// 超时或被取消不重试,避免无意义的重复请求

if (error.name === 'AbortError') {

throw new Error(\`请求超时(超过 ${timeout}ms)\`);

}

if (retries > 0) {

console.warn(\`请求失败,${retries} 次重试机会剩余:\`, error.message);

// 指数退避,避免瞬间打满服务器

const delay = (4 - retries) * 500;

await new Promise(resolve => setTimeout(resolve, delay));

return fetchWithRetry(url, options, retries - 1, timeout);

}

throw error;

}

}

// 使用示例

fetchWithRetry('/api/users', { method: 'GET' })

.then(data => console.log('获取成功:', data))

.catch(err => console.error('最终失败:', err.message));

\`\`\`

代码示例:统一的请求封装(携带 token、区分 JSON/表单)

\`\`\`javascript

class ApiClient {

constructor(baseURL, getToken) {

this.baseURL = baseURL;

this.getToken = getToken;

}

async request(path, { method = 'GET', body, headers = {}, ...rest } = {}) {

const token = this.getToken?.();

const finalHeaders = {

'Content-Type': 'application/json',

...(token ? { Authorization: \`Bearer ${token}\` } : {}),

...headers

};

const response = await fetch(\`${this.baseURL}${path}\`, {

method,

headers: finalHeaders,

body: body ? JSON.stringify(body) : undefined,

...rest

});

if (response.status === 401) {

// 统一处理登录失效

window.location.href = '/login';

throw new Error('登录已过期');

}

if (!response.ok) {

throw new Error(\`请求失败: ${response.status}\`);

}

return response.status === 204 ? null : response.json();

}

get(path, options) { return this.request(path, { ...options, method: 'GET' }); }

post(path, body, options) { return this.request(path, { ...options, method: 'POST', body }); }

}

const api = new ApiClient('https://api.example.com', () => localStorage.getItem('token'));

api.get('/user/profile').then(console.log);

\`\`\`

⚡ HTTP/1.1 vs HTTP/2 vs HTTP/3

这是前端面试和性能优化里绕不开的话题。三代协议解决的核心问题一脉相承:如何在一条(或几条)连接上,更快、更并发地传输更多资源

HTTP/1.1 的问题:队头阻塞(Head-of-Line Blocking)

HTTP/1.1 虽然支持持久连接(Keep-Alive)和管道化(Pipelining),但同一条 TCP 连接上的请求必须严格按顺序返回响应——如果排在前面的请求迟迟没有响应,后面即使已经处理完了,也必须排队等待。为了缓解这个问题,浏览器通常对同一域名开启 6~8 条并行 TCP 连接,但这又带来了额外的握手开销、以及服务器资源压力,也是"域名分片"(把资源分散到多个子域名)这种老技巧的由来。

HTTP/2 的解决方案:多路复用

HTTP/2 引入了二进制分帧层,把请求和响应拆分成更小的帧(Frame),同一个 TCP 连接上可以并行交错传输多个请求/响应的帧,接收方再根据帧头的流 ID(Stream ID)重新组装。这样一条连接就能真正并发处理多个请求,不再需要开多条 TCP 连接。

HTTP/2 还带来:

头部压缩(HPACK):用哈夫曼编码和索引表压缩重复的请求头(如 User-Agent、Cookie),减少冗余传输
服务器推送(Server Push):服务器可以主动把客户端还没请求但很可能需要的资源推送过去(实践中因缓存复杂度问题,各大浏览器已逐步废弃或限制该特性)
流优先级:客户端可以告诉服务器哪些资源更重要,优先返回

但 HTTP/2 依然跑在 TCP 之上,这带来一个新问题:TCP 层面的队头阻塞。TCP 保证字节流有序到达,一旦某个 TCP 包丢失,即使 HTTP/2 层面的其他流数据已经到达,也必须等待丢失的包重传,所有流都会被阻塞——这是 HTTP/2 无法从应用层解决的问题,因为它出在传输层。

HTTP/3 的解决方案:基于 QUIC

HTTP/3 放弃 TCP,改用QUIC协议(构建在 UDP 之上),从根本上解决了 TCP 层队头阻塞:

QUIC 内部实现了多路复用的流,每个流独立进行丢包重传,一个流丢包不会影响其他流
连接迁移:QUIC 用连接 ID 而不是四元组(源 IP、源端口、目的 IP、目的端口)标识连接,手机从 WiFi 切换到 4G 时连接不会中断
1-RTT 甚至 0-RTT 建连:QUIC 把传输层握手和 TLS 1.3 握手合并在一起完成,比 TCP+TLS1.2 的组合快得多
内置加密:QUIC 强制加密,没有明文的 QUIC

三代协议对比表:

| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |

| --- | --- | --- | --- |

| 传输层协议 | TCP | TCP | QUIC(基于 UDP) |

| 数据格式 | 文本 | 二进制分帧 | 二进制分帧 |

| 多路复用 | 不支持(需多条连接) | 支持(单连接内) | 支持,且流之间互不阻塞 |

| 队头阻塞 | 应用层存在 | 应用层解决,传输层仍存在 | 完全解决 |

| 头部压缩 | 无 | HPACK | QPACK |

| 服务器推送 | 不支持 | 支持(已趋于废弃) | 支持 |

| 连接迁移 | 不支持 | 不支持 | 支持(网络切换不断连) |

| 建连速度 | TCP 握手 + TLS 握手,串行 | 同 HTTP/1.1 | 传输层与 TLS1.3 握手合并 |

| 部署难度 | 简单 | 简单,多数 CDN 已默认开启 | 需要服务器与 CDN 支持 QUIC,部分网络环境屏蔽 UDP |

代码示例:检测当前请求使用的协议版本

\`\`\`javascript

// 通过 Resource Timing API 查看协议

performance.getEntriesByType('resource').forEach(entry => {

console.log(entry.name, entry.nextHopProtocol);

// 常见取值: "http/1.1"、"h2"、"h3"

});

// 也可以在 Network 面板中新增 "Protocol" 列直接查看

\`\`\`

🔒 HTTPS 与 TLS

HTTP 是明文传输的,任何中间节点(路由器、运营商、公共 WiFi 的攻击者)都能窃听甚至篡改数据。HTTPS = HTTP + TLS(Transport Layer Security,其前身是 SSL),通过加密解决了三个核心问题:

机密性:数据被加密,中间人即使截获也看不懂内容
完整性:通过消息认证码(MAC)确保数据没有被篡改
身份认证:通过证书确认你连接的确实是目标服务器,而不是冒充者(防止中间人攻击)

对称加密 vs 非对称加密

TLS 握手的核心目标,是让客户端和服务器安全地协商出一份只有双方知道的对称密钥,之后正式传输数据时用这份对称密钥加解密(因为对称加密速度快得多)。而协商密钥的过程本身,则依赖非对称加密(公钥加密、私钥解密)来防止密钥被窃听。

| 加密方式 | 特点 | 在 TLS 中的角色 |

| --- | --- | --- |

| 对称加密(如 AES) | 加解密用同一把密钥,速度快 | 用于实际数据传输的加解密 |

| 非对称加密(如 RSA、ECDHE) | 公钥加密、私钥解密(或反过来),速度慢但能安全交换密钥 | 用于握手阶段协商对称密钥、验证身份 |

数字证书与证书链

服务器需要向客户端证明"我就是 example.com",这靠数字证书完成:

1.网站运营者生成一对公私钥,把公钥和域名信息提交给证书颁发机构(CA)申请证书
2.CA 验证域名所有权后,用自己的私钥对证书内容签名,颁发数字证书
3.浏览器内置了一批受信任的根证书,通过验证签名链(服务器证书 → 中间证书 → 根证书),确认证书是可信 CA 颁发的,且证书没有被篡改
4.如果证书链验证失败(过期、域名不匹配、自签名且不受信任),浏览器会显示"您的连接不是私密连接"这类警告

\`\`\`bash

# 查看某个网站的证书链

openssl s_client -connect example.com:443 -showcerts

# 查看证书的有效期、颁发者等信息

openssl x509 -in cert.pem -noout -dates -issuer -subject

\`\`\`

TLS 1.2 vs TLS 1.3:握手往返次数的巨大差异

这是一个非常值得记住的性能优化点:TLS 版本升级能实打实地减少首次连接的延迟。

TLS 1.2 握手(需要 2 个 RTT):

\`\`\`text

客户端 服务器

|--- ClientHello(支持的加密套件、随机数)------->|

|<-- ServerHello + 证书 + ServerKeyExchange ------|

|<-- ServerHelloDone -----------------------------|

|--- ClientKeyExchange + ChangeCipherSpec -------->|

|--- Finished ------------------------------------->|

|<-- ChangeCipherSpec + Finished ------------------|

(此时才能开始传输加密后的应用数据,共耗费 2 个 RTT)

\`\`\`

TLS 1.3 握手(只需 1 个 RTT,支持场景下甚至 0-RTT):

\`\`\`text

客户端 服务器

|--- ClientHello(直接带上猜测的密钥交换参数)---->|

|<-- ServerHello + 密钥交换参数 + 加密后的证书 -----|

|<-- Finished --------------------------------------|

|--- Finished ---------------------------------------->|

(1 个 RTT 后即可开始传输应用数据)

如果是"回头客"(此前连接过,服务器颁发了 Session Ticket):

|--- ClientHello + 提前预测的应用数据(0-RTT)----->|

(无需等待往返,直接携带数据,但 0-RTT 数据存在重放攻击风险,仅适合幂等请求)

\`\`\`

握手往返对比表:

| 版本 | 握手往返(首次连接) | 握手往返(会话恢复) | 支持的密钥交换 | 安全性 |

| --- | --- | --- | --- | --- |

| TLS 1.0 / 1.1 | 2-RTT | 1-RTT | RSA、DH | 已被主流浏览器淘汰 |

| TLS 1.2 | 2-RTT | 1-RTT(Session ID/Ticket) | RSA、ECDHE | 仍广泛使用,但握手较慢 |

| TLS 1.3 | 1-RTT | 0-RTT(PSK 恢复) | 仅前向安全的 ECDHE 类 | 更快、更安全,移除了不安全的算法 |

代码示例:Node.js 服务端强制使用 TLS 1.3,并查看协商结果

\`\`\`javascript

const https = require('https');

const fs = require('fs');

const server = https.createServer({

key: fs.readFileSync('key.pem'),

cert: fs.readFileSync('cert.pem'),

minVersion: 'TLSv1.2', // 建议至少 1.2,有条件应设为 1.3

ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384'

}, (req, res) => {

res.end('hello https');

});

server.on('secureConnection', (tlsSocket) => {

console.log('协商的 TLS 版本:', tlsSocket.getProtocol());

console.log('加密套件:', tlsSocket.getCipher());

});

server.listen(443);

\`\`\`

HTTPS 握手 + HTTP 请求的完整时序(首次访问):

1.TCP 三次握手(1-RTT)
2.TLS 握手(TLS1.2 为 2-RTT,TLS1.3 为 1-RTT)
3.发送 HTTP 请求,等待响应(1-RTT + 服务器处理时间)

也就是说,同样的网络条件下,从 TLS1.2 升级到 TLS1.3,理论上首次访问能省下整整 1 个 RTT——在跨国网络中,1 个 RTT 可能就是 100ms 以上,这对首屏体验的提升是非常可观的。

🔌 WebSocket、SSE 与长轮询

当业务需要"服务器主动、持续地向客户端推送数据"(比如聊天消息、股票行情、直播弹幕),普通的一问一答式 HTTP 就不够用了,常见的三种方案各有取舍:

| 方案 | 通信方向 | 连接方式 | 实时性 | 适用场景 | 兼容性 |

| --- | --- | --- | --- | --- | --- |

| 长轮询(Long Polling) | 客户端发起,服务器 hold 住直到有数据 | 每次请求后立即发起下一次 | 较好,但有请求间隙 | 兼容性要求极高的老系统 | 极好,基于普通 HTTP |

| SSE(Server-Sent Events) | 服务器 → 客户端单向推送 | 一条长连接,基于 HTTP | 好 | 通知、实时日志、行情播报等单向场景 | 良好(IE 不支持,需 polyfill) |

| WebSocket | 全双工,双向都能主动发 | 独立的持久连接,握手后升级协议 | 最好 | 聊天、协作编辑、实时游戏、弹幕 | 良好 |

WebSocket 握手过程

WebSocket 巧妙地"借用"了 HTTP 完成握手,再"升级"为独立的 WebSocket 协议:

\`\`\`text

客户端请求:

GET /chat HTTP/1.1

Host: example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Sec-WebSocket-Version: 13

服务器响应:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

(之后这条 TCP 连接不再走 HTTP 语义,改为 WebSocket 帧协议,双方都能随时主动发消息)

\`\`\`

代码示例:带心跳与断线重连的 WebSocket 客户端封装

真实项目里裸用原生 WebSocket 会遇到网络抖动断连、服务端假死等问题,通常需要封装心跳检测和自动重连:

\`\`\`javascript

class ReconnectingWebSocket {

constructor(url, options = {}) {

this.url = url;

this.heartbeatInterval = options.heartbeatInterval || 30000;

this.reconnectDelay = options.reconnectDelay || 3000;

this.maxReconnectAttempts = options.maxReconnectAttempts || 10;

this.reconnectAttempts = 0;

this.listeners = {};

this.manualClose = false;

this.connect();

}

connect() {

this.ws = new WebSocket(this.url);

this.ws.onopen = () => {

console.log('WebSocket 已连接');

this.reconnectAttempts = 0;

this.startHeartbeat();

this.emit('open');

};

this.ws.onmessage = (event) => {

if (event.data === 'pong') {

return; // 心跳响应,不向业务层暴露

}

try {

this.emit('message', JSON.parse(event.data));

} catch {

this.emit('message', event.data);

}

};

this.ws.onclose = () => {

this.stopHeartbeat();

this.emit('close');

if (!this.manualClose) {

this.reconnect();

}

};

this.ws.onerror = (error) => {

this.emit('error', error);

this.ws.close();

};

}

reconnect() {

if (this.reconnectAttempts >= this.maxReconnectAttempts) {

console.error('已达最大重连次数,停止重连');

this.emit('reconnect-failed');

return;

}

this.reconnectAttempts++;

// 简单的退避策略:重连次数越多,等待时间越长(有上限)

const delay = this.reconnectDelay * Math.min(this.reconnectAttempts, 5);

console.log(\`${delay}ms 后进行第 ${this.reconnectAttempts} 次重连\`);

setTimeout(() => this.connect(), delay);

}

startHeartbeat() {

this.heartbeatTimer = setInterval(() => {

if (this.ws.readyState === WebSocket.OPEN) {

this.ws.send('ping');

}

}, this.heartbeatInterval);

}

stopHeartbeat() {

clearInterval(this.heartbeatTimer);

}

send(data) {

if (this.ws.readyState === WebSocket.OPEN) {

this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));

} else {

console.warn('WebSocket 未连接,无法发送数据');

}

}

close() {

this.manualClose = true;

this.stopHeartbeat();

this.ws.close();

}

on(event, callback) {

(this.listeners[event] = this.listeners[event] || []).push(callback);

}

emit(event, data) {

(this.listeners[event] || []).forEach(cb => cb(data));

}

}

// 使用示例:直播弹幕接收

const danmuSocket = new ReconnectingWebSocket('wss://live.example.com/danmu');

danmuSocket.on('open', () => console.log('弹幕通道已连接'));

danmuSocket.on('message', (data) => renderDanmu(data));

danmuSocket.on('reconnect-failed', () => showToast('弹幕连接失败,请刷新页面'));

\`\`\`

代码示例:EventSource(SSE)用法

SSE 天然支持断线自动重连(浏览器内置),非常适合"服务器 → 客户端"的单向推送场景,比如价格变动通知:

\`\`\`javascript

const evtSource = new EventSource('/api/notifications');

// 默认的 message 事件

evtSource.onmessage = (event) => {

console.log('收到通知:', event.data);

};

// 自定义事件名(服务端通过 event: price-update 指定)

evtSource.addEventListener('price-update', (event) => {

const data = JSON.parse(event.data);

updatePriceUI(data);

});

evtSource.onerror = (error) => {

if (evtSource.readyState === EventSource.CLOSED) {

console.log('SSE 连接已关闭');

} else {

console.warn('SSE 连接出错,浏览器将按规范自动重连', error);

}

};

// 服务端对应的响应格式(Node/Express 示例):

// res.writeHead(200, {

// 'Content-Type': 'text/event-stream',

// 'Cache-Control': 'no-cache',

// Connection: 'keep-alive'

// });

// res.write('event: price-update\\n');

// res.write(\`data: ${JSON.stringify({ price: 123.45 })}\\n\\n\`);

// 主动关闭连接

// evtSource.close();

\`\`\`

代码示例:长轮询实现

\`\`\`javascript

async function longPolling(url, onData) {

try {

// 服务端会 hold 住这个请求,直到有新数据或超时

const response = await fetch(url, { method: 'GET' });

if (response.ok) {

const data = await response.json();

onData(data);

}

} catch (error) {

console.error('长轮询请求失败,1 秒后重试:', error);

} finally {

// 无论成功、失败还是超时,都立即发起下一轮请求

setTimeout(() => longPolling(url, onData), 1000);

}

}

longPolling('/api/poll/messages', (data) => {

console.log('收到新消息:', data);

});

\`\`\`

真实案例:直播弹幕系统为什么选 WebSocket

某直播平台早期用 HTTP 短轮询(每 2 秒轮询一次弹幕列表)实现弹幕功能,随着并发观众数增长到十万级别,服务器被高频轮询请求压垮,且弹幕延迟高达 2 秒以上,观感很差。切换到 WebSocket 后:

单条 WebSocket 长连接代替了每 2 秒一次的 HTTP 请求,服务器连接数虽然增加,但请求总量大幅下降(不再有轮询请求的头部开销)
弹幕延迟从"平均 1~2 秒"降低到"100ms 以内"
结合心跳检测 + 断线重连,弱网环境下的弹幕体验也更稳定

而对于类似"限时秒杀库存变化通知"这种单向、低频的场景,很多团队会优先选择 SSE,因为它实现更简单、能复用 HTTP 基础设施(如网关、负载均衡),不需要额外维护 WebSocket 的连接状态。

💾 浏览器缓存机制

缓存是前端性能优化里投入产出比最高的手段之一。浏览器缓存分为强缓存协商缓存两大类,二者的核心区别是:强缓存命中时完全不发请求,协商缓存命中时仍发请求,但服务器返回 304 且不带响应体

强缓存:Cache-Control 与 Expires

\`Expires\`:HTTP/1.0 遗留字段,指定一个绝对过期时间(如 \`Wed, 21 Oct 2026 07:28:00 GMT\`),缺点是依赖客户端本地时间,容易因为时钟不准出问题
\`Cache-Control\`:HTTP/1.1 引入,优先级高于 \`Expires\`,常用值:

- \`max-age=3600\`:缓存 3600 秒内有效,用相对时间,不受本地时钟影响

- \`no-cache\`:不是"不缓存",而是"缓存了,但每次都要向服务器验证是否过期"(走协商缓存流程)

- \`no-store\`:真正的完全不缓存,每次都重新请求完整资源

- \`public\`:允许被 CDN、代理服务器等中间节点缓存

- \`private\`:只允许浏览器本地缓存,不允许中间代理缓存(常用于带用户身份的响应)

- \`immutable\`:告诉浏览器该资源永不改变,即使用户手动刷新也不必重新验证(配合文件名带 hash 使用效果最佳)

协商缓存:ETag 与 Last-Modified

\`Last-Modified\` / \`If-Modified-Since\`:服务器返回资源的最后修改时间,浏览器下次请求时带上 \`If-Modified-Since\`,服务器比较时间,未修改则返回 304。缺点是精度只到秒级,且"内容没变但修改时间变了"(比如文件被重新保存但内容一致)也会被判定为已修改
\`ETag\` / \`If-None-Match\`:服务器根据内容生成一个哈希指纹作为 \`ETag\`,浏览器下次请求带上 \`If-None-Match\`,服务器比较指纹是否一致。优点是精确到内容本身,缺点是每次生成哈希对服务器有一定计算开销
当二者同时存在时,\`ETag\` 优先级更高

强缓存 vs 协商缓存对比表:

| 对比项 | 强缓存 | 协商缓存 |

| --- | --- | --- |

| 是否发请求 | 否,直接用本地缓存 | 是,但只传头部信息,不传响应体 |

| 相关响应头 | Cache-Control、Expires | ETag/If-None-Match、Last-Modified/If-Modified-Since |

| 命中时状态码 | 200(from disk/memory cache,DevTools 中可见) | 304 Not Modified |

| 精确度 | 依赖设定的过期时间,不感知内容是否真变化 | 可以精确判断内容是否变化 |

| 典型使用场景 | 带 hash 文件名的静态资源(js/css/图片) | HTML 入口文件、API 数据接口 |

缓存存放位置:

| 位置 | 特点 |

| --- | --- |

| Memory Cache(内存缓存) | 速度最快,但随进程/标签页关闭而失效,容量小 |

| Disk Cache(磁盘缓存) | 速度慢于内存缓存,但容量大、持久性好,多数资源最终会落到这里 |

| Service Worker Cache | 由开发者用代码完全控制的缓存,离线场景的核心能力 |

| Push Cache | HTTP/2 Server Push 推送资源的临时缓存,生命周期很短(仅在同一个 session 内) |

代码示例:Service Worker 实现自定义缓存策略

Service Worker 允许你像写中间件一样拦截页面的每一个网络请求,自由决定缓存策略(这是普通 Cache-Control 做不到的灵活度):

\`\`\`javascript

// sw.js

const CACHE_NAME = 'app-cache-v1';

const STATIC_ASSETS = ['/', '/index.html', '/styles.css', '/app.js'];

// 安装阶段:预缓存核心静态资源

self.addEventListener('install', (event) => {

event.waitUntil(

caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))

);

self.skipWaiting();

});

// 激活阶段:清理旧版本缓存

self.addEventListener('activate', (event) => {

event.waitUntil(

caches.keys().then(keys =>

Promise.all(

keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))

)

)

);

self.clients.claim();

});

// 拦截请求:不同类型资源采用不同策略

self.addEventListener('fetch', (event) => {

const { request } = event;

// 图片:缓存优先(Cache First),命中直接返回,未命中再请求网络并写入缓存

if (request.destination === 'image') {

event.respondWith(

caches.match(request).then(cached => {

if (cached) return cached;

return fetch(request).then(response => {

const clone = response.clone();

caches.open(CACHE_NAME).then(cache => cache.put(request, clone));

return response;

});

})

);

return;

}

// 页面导航:网络优先(Network First),失败时降级到缓存的离线页

if (request.mode === 'navigate') {

event.respondWith(

fetch(request).catch(() => caches.match('/index.html'))

);

return;

}

// 其他请求:先查缓存,没有再走网络(Stale-While-Revalidate 的简化版)

event.respondWith(

caches.match(request).then(cached => cached || fetch(request))

);

});

\`\`\`

代码示例:注册 Service Worker

\`\`\`javascript

if ('serviceWorker' in navigator) {

window.addEventListener('load', () => {

navigator.serviceWorker.register('/sw.js')

.then(reg => console.log('SW 注册成功,作用域:', reg.scope))

.catch(err => console.error('SW 注册失败:', err));

});

}

\`\`\`

🌐 CORS 跨域资源共享

同源策略回顾:浏览器出于安全考虑,默认限制一个源(协议 + 域名 + 端口三者完全一致才算同源)的脚本访问另一个源的资源。CORS(Cross-Origin Resource Sharing)就是浏览器和服务器之间的一套"白名单协商机制",让服务器可以显式声明"我允许哪些源访问我"。

简单请求 vs 预检请求

浏览器会先判断这个跨域请求是不是"简单请求",如果不是,就要先发一个 \`OPTIONS\` 预检请求确认服务器允许后,再发真正的请求。

判定为简单请求需要同时满足:

方法只能是 \`GET\`、\`POST\`、\`HEAD\` 之一
请求头只能包含几个"安全"的头(如 \`Accept\`、\`Content-Type\` 且值仅限于 \`text/plain\`、\`multipart/form-data\`、\`application/x-www-form-urlencoded\`)
没有使用 \`ReadableStream\` 等特殊 API

只要不满足上述任意一条(比如用 \`Content-Type: application/json\`、自定义了 \`Authorization\` 头、或者用了 \`PUT\`/\`DELETE\` 方法),就会触发预检请求

\`\`\`text

简单请求流程:

浏览器 ---(直接发送真实请求,带 Origin 头)---> 服务器

浏览器 <---(响应带 Access-Control-Allow-Origin)--- 服务器

浏览器根据响应头判断是否允许读取响应内容

预检请求流程:

浏览器 ---(OPTIONS 预检,询问是否允许)---> 服务器

浏览器 <---(204/200,带 Access-Control-Allow-* 一系列头)--- 服务器

浏览器确认允许后,才真正发出请求

浏览器 ---(真实请求)---> 服务器

浏览器 <---(真实响应)--- 服务器

\`\`\`

关键响应头一览:

| 响应头 | 作用 |

| --- | --- |

| Access-Control-Allow-Origin | 允许访问的源,可以是具体域名或 \`\`(但携带凭证时不能用 \`\`) |

| Access-Control-Allow-Methods | 允许的 HTTP 方法列表 |

| Access-Control-Allow-Headers | 允许携带的自定义请求头 |

| Access-Control-Allow-Credentials | 是否允许携带 Cookie 等凭证,值只能是 \`true\` |

| Access-Control-Max-Age | 预检请求结果的缓存时间(秒),减少重复预检 |

| Access-Control-Expose-Headers | 允许 JS 通过 \`response.headers.get()\` 读取的额外响应头(默认只能读取几个基础头) |

withCredentials:跨域携带 Cookie

默认情况下,跨域请求不会携带 Cookie。如果需要携带(比如跨域调用同公司不同子域名下的登录态接口),需要客户端和服务端同时配置:

\`\`\`javascript

// 客户端:fetch 需要显式声明携带凭证

fetch('https://api.example.com/user/profile', {

method: 'GET',

credentials: 'include' // 对应 XMLHttpRequest 里的 withCredentials = true

});

// XMLHttpRequest 写法

const xhr = new XMLHttpRequest();

xhr.open('GET', 'https://api.example.com/user/profile');

xhr.withCredentials = true;

xhr.send();

\`\`\`

代码示例:Node/Express 服务端 CORS 配置

\`\`\`javascript

const express = require('express');

const app = express();

// 手动实现(不依赖第三方库),便于理解每个头的作用

app.use((req, res, next) => {

const allowedOrigins = ['https://example.com', 'https://admin.example.com'];

const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {

// 携带凭证时,Allow-Origin 不能写 *,必须回显具体的源

res.setHeader('Access-Control-Allow-Origin', origin);

}

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');

res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

res.setHeader('Access-Control-Allow-Credentials', 'true');

res.setHeader('Access-Control-Max-Age', '86400'); // 预检结果缓存一天,减少重复 OPTIONS 请求

if (req.method === 'OPTIONS') {

return res.sendStatus(204); // 预检请求直接返回,不进入业务逻辑

}

next();

});

// 使用官方 cors 中间件的等价写法

// const cors = require('cors');

// app.use(cors({

// origin: ['https://example.com', 'https://admin.example.com'],

// credentials: true,

// maxAge: 86400

// }));

\`\`\`

常见坑:

\`Access-Control-Allow-Origin: *\` 与 \`credentials: include\` 不能同时生效,浏览器会直接拦截响应
预检请求失败往往是因为服务器没有正确处理 \`OPTIONS\` 方法(比如被鉴权中间件拦截返回了 401,导致预检本身失败)
\`Access-Control-Allow-Headers\` 遗漏了实际发送的自定义头(如 \`X-Request-Id\`),会导致预检通过但浏览器仍然拒绝发送真实请求
CORS 只是浏览器侧的限制,服务器本身并没有被"保护"——用 Postman、curl 等工具是不受同源策略限制的,CORS 解决的是"防止恶意网页利用用户身份发起请求",而不是接口本身的安全

🚀 网络性能优化

减少请求数量:

合并小图标为雪碧图(Sprite)或使用 SVG Symbol / 图标字体
合并 CSS/JS 文件(但 HTTP/2 多路复用场景下,过度合并反而可能失去"按需加载、并行下载"的优势,需要按场景权衡)
内联关键 CSS(Critical CSS),避免额外的渲染阻塞请求
减少不必要的第三方脚本(统计埋点、广告 SDK 等,每一个都是一次额外的 DNS+TCP+TLS+下载)

压缩:gzip vs brotli

对文本类资源(HTML/CSS/JS/JSON)开启压缩,通常能带来数量级的体积下降:

| 压缩算法 | 典型压缩率(相对原始文本) | 压缩速度 | 浏览器支持 | 说明 |

| --- | --- | --- | --- | --- |

| 不压缩 | 0%(原始大小) | - | - | 基线 |

| gzip | 通常减少 60%~70% 体积 | 快 | 几乎全部浏览器 | 兼容性最好,是长期以来的默认选择 |

| brotli | 通常比 gzip 再多减少 15%~20% 体积 | 压缩较慢(可预压缩静态资源缓解),解压快 | 现代浏览器(IE 不支持) | 对文本类资源效果尤其好,是目前的推荐首选 |

代码示例:Nginx 同时开启 gzip 与 brotli

\`\`\`text

# gzip 配置(兼容性兜底)

gzip on;

gzip_types text/plain text/css application/javascript application/json image/svg+xml;

gzip_min_length 1024;

gzip_comp_level 6;

# brotli 配置(需要 ngx_brotli 模块,优先于 gzip 被现代浏览器使用)

brotli on;

brotli_types text/plain text/css application/javascript application/json image/svg+xml;

brotli_comp_level 6;

# 对于构建时已生成的静态资源,也可以预先生成 .br/.gz 文件,运行时直接命中,

# 避免每次请求都实时压缩消耗 CPU(static_gzip / static_brotli 模块)

\`\`\`

CDN:就近分发,大幅降低 TTFB

CDN(内容分发网络)把静态资源缓存到全球各地的边缘节点,用户请求会被调度到"距离最近、速度最快"的节点,而不是每次都要跨国访问源站。

| 场景 | 平均 TTFB(示例数据,仅作量级参考) |

| --- | --- |

| 用户直接访问源站(跨国,如国内用户访问美国服务器) | 300ms ~ 800ms |

| 用户访问就近 CDN 边缘节点(命中缓存) | 20ms ~ 80ms |

| CDN 节点未命中缓存,需回源 | 与直接访问源站相近,仅首次请求或缓存失效后发生 |

真实案例:大型电商首屏加速

某电商平台大促前对首页做过一轮网络层优化,主要动作包括:接入 CDN 分发首屏图片和静态资源、把 API 网关升级到 HTTP/2、对文本资源全面开启 brotli 压缩、给核心静态资源加上 \`Cache-Control: public, max-age=31536000, immutable\`(配合文件名 hash)。上线后主要收益:

首屏关键资源的平均 TTFB 从原来的约 400ms 降低到 60ms 左右(CDN 命中后)
首屏 JS/CSS 总传输体积因为 brotli 压缩相比 gzip 又减少了约 15%
大促当天源站的回源请求量因为 CDN 缓存命中率提升而大幅下降,减轻了源站压力,间接提升了未命中缓存请求的响应速度

资源提示(Resource Hints)

浏览器提供了一组 \`\` 提示,让开发者可以"剧透"接下来即将发生的网络请求,提前完成部分工作:

| 提示 | 提前完成的工作 | 开销 | 适用场景 |

| --- | --- | --- | --- |

| dns-prefetch | 仅 DNS 解析 | 最小 | 会用到但优先级不高的第三方域名 |

| preconnect | DNS + TCP 握手 + TLS 握手 | 较小,但同时开太多连接反而占用资源 | 明确接下来一定会请求的关键域名(如接口网关、字体 CDN) |

| preload | 完整下载该资源,且提高其加载优先级 | 较大,会抢占带宽 | 当前页面一定会用到的关键资源(字体、首屏图、关键 JS) |

| prefetch | 在浏览器空闲时下载资源 | 较小,优先级最低 | 用户大概率要跳转的下一页资源 |

| prerender | 后台完整渲染整个页面 | 最大 | 极高置信度的下一步跳转(如搜索结果第一条) |

代码示例:资源提示的实际写法

\`\`\`html

\`\`\`

常见坑:

\`preload\` 用多了会和当前页面的关键资源抢带宽,反而拖慢首屏,只用于真正关键的资源
\`preconnect\` 对同一个域名最多建立几条连接就够了,滥用会浪费客户端和服务器资源,且长期不使用的预连接会被浏览器主动断开
字体资源用 \`preload\` 时一定要加 \`crossorigin\` 属性(即使同源也要加),否则会被当成一次单独的请求重新下载,预加载失效

📊 网络监控与关键性能指标

要优化网络性能,第一步永远是"先测量"。浏览器提供的 Navigation Timing API 和 Resource Timing API,能精确拿到 DNS、TCP、TLS、TTFB 等每个阶段的耗时。

代码示例:用 Navigation Timing API 采集关键网络指标

\`\`\`javascript

function getNetworkTimings() {

const [nav] = performance.getEntriesByType('navigation');

if (!nav) return null;

return {

dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart),

tcp: Math.round(nav.connectEnd - nav.connectStart),

// secureConnectionStart 为 0 表示没有走 TLS(即普通 HTTP)

tls: nav.secureConnectionStart > 0

? Math.round(nav.connectEnd - nav.secureConnectionStart)

: 0,

ttfb: Math.round(nav.responseStart - nav.requestStart),

contentDownload: Math.round(nav.responseEnd - nav.responseStart),

domReady: Math.round(nav.domContentLoadedEventEnd - nav.startTime),

fullLoad: Math.round(nav.loadEventEnd - nav.startTime)

};

}

console.table(getNetworkTimings());

// 示例输出:

// { dns: 12, tcp: 28, tls: 45, ttfb: 180, contentDownload: 35, domReady: 620, fullLoad: 980 }

\`\`\`

代码示例:用 Resource Timing API 找出慢资源

\`\`\`javascript

function findSlowResources(thresholdMs = 500) {

return performance.getEntriesByType('resource')

.filter(entry => entry.duration > thresholdMs)

.map(entry => ({

url: entry.name,

type: entry.initiatorType,

duration: Math.round(entry.duration),

transferSize: entry.transferSize,

protocol: entry.nextHopProtocol

}))

.sort((a, b) => b.duration - a.duration);

}

console.table(findSlowResources());

// 持续监控新加载的资源(适合 SPA 场景,页面运行期间动态请求也能捕获)

const resourceObserver = new PerformanceObserver((list) => {

list.getEntries().forEach(entry => {

if (entry.duration > 500) {

console.warn(\`慢资源: ${entry.name}, 耗时 ${entry.duration.toFixed(0)}ms\`);

}

// 上报到监控平台

reportToAnalytics({

name: entry.name,

type: entry.initiatorType,

duration: entry.duration,

transferSize: entry.transferSize,

protocol: entry.nextHopProtocol

});

});

});

resourceObserver.observe({ entryTypes: ['resource'], buffered: true });

function reportToAnalytics(data) {

// 用 sendBeacon 上报,不阻塞页面卸载,也不影响主流程性能

navigator.sendBeacon('/api/perf-report', JSON.stringify(data));

}

\`\`\`

常用网络监控工具:

| 工具 | 定位 |

| --- | --- |

| Chrome DevTools Network 面板 | 开发调试阶段查看单次请求的详细时序瀑布图 |

| Lighthouse | 自动化审计工具,给出性能评分与具体优化建议 |

| WebPageTest | 支持多地域、多网络环境(弱网模拟)的详细性能测试 |

| Real User Monitoring(RUM,如 New Relic、Sentry Performance) | 采集真实用户的网络数据,反映线上真实情况,而不仅是实验室数据 |

网络错误处理与降级策略:

超时处理:所有网络请求都应该设置合理的超时时间,避免请求无限期挂起(见前文 \`AbortController\` 示例)
重试机制:对幂等请求(GET、PUT 等)设置有限次数的指数退避重试,非幂等请求(如支付提交)要格外谨慎,避免重试导致重复下单等问题
错误提示:区分"网络断开"、"服务器错误"、"业务逻辑错误",给用户展示有意义的提示,而不是一律"请求失败"
降级策略:关键功能在网络异常时应有兜底方案,比如展示本地缓存的旧数据并提示"当前展示为缓存内容",而不是白屏

✅ 最佳实践总结

协议选择:

全站启用 HTTPS,且尽量升级到 TLS 1.3,减少握手往返
有条件的话升级到 HTTP/2 甚至 HTTP/3,充分利用多路复用能力,减少"域名分片"这类为兼容 HTTP/1.1 而做的历史妥协
实时通信场景按需选择:双向高频用 WebSocket,单向推送用 SSE,兼容性优先且频率不高可以接受长轮询

缓存策略:

静态资源(文件名带 hash):\`Cache-Control: public, max-age=31536000, immutable\`,一年强缓存 + 内容变化即改文件名
HTML 入口文件:通常设置协商缓存或短强缓存(如 \`no-cache\` 或 \`max-age=0\`),确保用户总能拿到最新的资源引用清单
API 数据接口:根据数据实时性要求,酌情使用短时强缓存或协商缓存,减轻服务器压力
离线场景用 Service Worker 精细控制缓存策略,区分"缓存优先"和"网络优先"的资源类型

性能优化:

减少请求数量和体积:合并资源、精灵图、开启 gzip/brotli 压缩、图片使用 WebP/AVIF 等现代格式
合理使用资源提示(dns-prefetch/preconnect/preload/prefetch),但不要滥用抢占带宽
接入 CDN,让用户就近访问,显著降低 TTFB
持续监控 Navigation Timing / Resource Timing 等指标,建立线上性能基线和告警

安全实践:

全站 HTTPS,禁用不安全的 TLS 版本(如 TLS 1.0/1.1)
严格配置 CORS 白名单,避免 \`Access-Control-Allow-Origin: *\` 搭配敏感接口
关键 Cookie 设置 \`HttpOnly\`、\`Secure\`、\`SameSite\` 属性,防范 XSS 窃取和 CSRF 攻击
网络请求做好超时、重试、降级的兜底逻辑,避免因为网络异常直接导致页面不可用

📚 学习资源

网络协议:

《HTTP 权威指南》
《TCP/IP 详解》
MDN Web Docs(HTTP、WebSocket、Fetch 等专题文档)
RFC 文档(如 RFC 9110 HTTP Semantics、RFC 9114 HTTP/3、RFC 8446 TLS 1.3)

性能优化:

Web Vitals(web.dev/vitals)
Lighthouse 官方文档
Google Web Fundamentals
各大厂的前端性能优化实践分享

📌 核心概念速查表

| 主题 | 一句话总结 |

| --- | --- |

| DNS 解析 | 域名转 IP,逐级缓存可大幅减少查询延迟 |

| TCP 三次握手 | 双向确认收发能力,避免半开连接浪费资源 |

| TCP 四次挥手 | 全双工连接需双方各自确认关闭 |

| HTTP/1.1 | 持久连接但存在队头阻塞,靠开多条连接缓解 |

| HTTP/2 | 二进制分帧 + 多路复用,单连接解决应用层队头阻塞 |

| HTTP/3 | 基于 QUIC/UDP,彻底解决传输层队头阻塞,支持连接迁移 |

| TLS 1.2 | 握手需 2-RTT,仍广泛使用 |

| TLS 1.3 | 握手仅需 1-RTT,支持场景下 0-RTT,是当前推荐版本 |

| 证书链 | 服务器证书由中间证书担保,最终追溯到浏览器信任的根证书 |

| WebSocket | 全双工持久连接,适合高频双向实时通信 |

| SSE | 服务器单向推送,基于 HTTP,实现简单、复用基础设施 |

| 强缓存 | 命中时完全不发请求,靠 Cache-Control/Expires 控制 |

| 协商缓存 | 命中时返回 304,靠 ETag/Last-Modified 验证 |

| CORS | 浏览器与服务器协商跨域访问权限,简单请求直发、复杂请求先预检 |

| 资源提示 | dns-prefetch/preconnect/preload/prefetch 按需提前完成部分网络工作 |

| 网络监控 | 用 Navigation/Resource Timing API 量化 DNS/TCP/TLS/TTFB,先测量再优化 |