🎯 这篇文章你将学到什么?
- 物理层的全双工/半双工如何影响前端性能优化
- 数据链路层的MTU限制如何指导资源大小优化
- 网络层IP分片对前端性能的影响
- 传输层TCP特性如何优化前端请求
- 应用层HTTP协议的前端优化策略
- 从底层原理到实际代码的完整优化方案
- 为什么这些优化有效,背后的原理是什么
📚 第一章:从物理层开始理解前端性能优化
1.1 全双工 vs 半双工:为什么HTTP/2比HTTP/1.1快?
1.1.1 物理层的全双工和半双工
什么是半双工?
半双工(Half-Duplex):
┌─────────┐ ┌─────────┐
│ 设备A │ ←──────→ │ 设备B │
└─────────┘ └─────────┘
↑ ↑
└───────────────────┘
同一时刻只能一个方向传输
就像对讲机:
- 你说话时,对方只能听
- 对方说话时,你只能听
- 不能同时说和听
什么是全双工?
全双工(Full-Duplex):
┌─────────┐ ┌─────────┐
│ 设备A │ ──────→ │ 设备B │
│ │ ←────── │ │
└─────────┘ └─────────┘
↑ ↑
└───────────────────┘
可以同时双向传输
就像电话:
- 你可以同时说和听
- 对方也可以同时说和听
- 双向同时进行
为什么网线需要两对双绞线?
一对双绞线 = 半双工(只能单向)
两对双绞线 = 全双工(可以双向同时)
发送线:设备A → 设备B
接收线:设备A ← 设备B
这样就能同时发送和接收数据了!
1.1.2 HTTP/1.1的"半双工"问题
HTTP/1.1的请求-响应模式
javascript
// HTTP/1.1 的请求过程
浏览器发送请求:
┌─────────────────────────────────┐
│ GET /api/user HTTP/1.1 │
│ Host: example.com │
│ │
│ [等待响应...] │
│ │
│ ← HTTP/1.1 200 OK │
│ ← { "name": "John" } │
└─────────────────────────────────┘
问题:
1. 发送请求时,不能接收其他响应
2. 接收响应时,不能发送其他请求
3. 必须等待一个请求完成,才能发送下一个
实际代码中的表现
javascript
// ❌ HTTP/1.1 的问题
// 假设需要加载3个资源
// 第1个请求
fetch('/api/user') // 等待响应...
.then(() => {
// 第2个请求(必须等第1个完成)
fetch('/api/posts') // 等待响应...
.then(() => {
// 第3个请求(必须等第2个完成)
fetch('/api/comments') // 等待响应...
})
})
// 总时间 = 请求1时间 + 请求2时间 + 请求3时间
// 如果每个请求200ms,总共需要600ms
HTTP/1.1的队头阻塞(Head-of-Line Blocking)
浏览器 → 服务器
请求1: GET /api/user
请求2: GET /api/posts
请求3: GET /api/comments
HTTP/1.1的限制:
- 虽然可以建立多个TCP连接(通常6个)
- 但每个连接仍然是"半双工"模式
- 一个连接上,必须等请求1响应完,才能处理请求2
就像单车道:
┌─────────────────────────────────┐
│ [请求1] → [请求2] → [请求3] │
│ ↓ 等待 等待 │
│ [响应1] ← │
└─────────────────────────────────┘
1.1.3 HTTP/2的"全双工"优势
HTTP/2的多路复用(Multiplexing)
HTTP/2 使用单个TCP连接,但支持多路复用:
┌─────────────────────────────────────┐
│ TCP连接(全双工) │
│ │
│ 请求1 ────────────────────→ │
│ 请求2 ────────────────────→ │
│ 请求3 ────────────────────→ │
│ ←──────────────────── 响应1 │
│ ←──────────────────── 响应2 │
│ ←──────────────────── 响应3 │
│ │
│ 所有请求和响应可以同时进行! │
└─────────────────────────────────────┘
实际代码中的优势
javascript
// ✅ HTTP/2 的优势
// 所有请求可以同时发送
Promise.all([
fetch('/api/user'), // 同时发送
fetch('/api/posts'), // 同时发送
fetch('/api/comments') // 同时发送
]).then(([user, posts, comments]) => {
// 所有响应几乎同时到达
})
// 总时间 ≈ max(请求1时间, 请求2时间, 请求3时间)
// 如果每个请求200ms,总共只需要200ms(而不是600ms)
为什么HTTP/2能做到"全双工"?
1. 二进制分帧(Binary Framing)
- HTTP/1.1 是文本协议(ASCII)
- HTTP/2 是二进制协议
- 每个请求/响应被分成多个帧(Frame)
- 帧可以交错发送和接收
2. 流(Stream)的概念
- 每个请求是一个流
- 多个流可以在同一个连接上并行
- 每个帧都有流ID,可以区分属于哪个请求
3. 就像物理层的两对双绞线:
- 一对用于发送(多个请求帧)
- 一对用于接收(多个响应帧)
- 可以同时进行!
1.1.4 前端如何利用HTTP/2优化
1. 减少HTTP连接数
javascript
// ❌ HTTP/1.1 的做法(需要多个连接)
// 浏览器会为每个域名建立6个TCP连接
// 超过6个资源需要排队等待
// ✅ HTTP/2 的做法(单个连接即可)
// 所有资源通过一个TCP连接加载
// 不需要担心连接数限制
// 实际优化:
// 1. 合并域名(减少DNS查询)
// 2. 使用HTTP/2服务器
// 3. 利用多路复用特性
2. 资源优先级设置
html
<!-- HTTP/2 支持资源优先级 -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/main.js" as="script">
<!-- 浏览器会告诉服务器优先级 -->
<!-- 服务器可以调整帧的发送顺序 -->
3. 服务器推送(Server Push)
javascript
// HTTP/2 服务器推送
// 服务器可以在响应HTML时,主动推送CSS/JS
// 前端配置(Nginx示例)
// location / {
// http2_push /style.css;
// http2_push /app.js;
// }
// 优势:
// - 减少往返次数(RTT)
// - 提前加载关键资源
4. 实际性能对比
javascript
// 场景:加载10个资源,每个100ms
// HTTP/1.1(6个并发连接):
// 前6个:100ms
// 后4个:100ms(等待前6个完成)
// 总计:200ms
// HTTP/2(多路复用):
// 所有10个:100ms(同时发送和接收)
// 总计:100ms
// 性能提升:50%
1.2 数据传输的基础单位:为什么资源大小很重要?
1.2.1 物理层的传输单位
从二进制到字节
物理层传输的是二进制位(bit):
0 或 1
8个位 = 1个字节(Byte)
传输过程:
┌─────────────────────────────────┐
│ 数据: "Hello" │
│ │
│ 转换为二进制: │
│ 01001000 01100101 01101100 ... │
│ │
│ 通过网线传输(电信号): │
│ 高电平/低电平 → 1/0 │
│ │
│ 接收端还原: │
│ 二进制 → 字节 → "Hello" │
└─────────────────────────────────┘
传输速率的影响
假设网速:10 Mbps(10兆比特每秒)
1. 计算实际传输速度:
10 Mbps ÷ 8 = 1.25 MB/s(兆字节每秒)
2. 传输1MB的文件需要:
1 MB ÷ 1.25 MB/s = 0.8秒
3. 传输10MB的文件需要:
10 MB ÷ 1.25 MB/s = 8秒
结论:文件越大,传输时间越长!
1.2.2 前端资源大小优化策略
1. 代码压缩(Minification)
javascript
// ❌ 未压缩的代码(2KB)
function calculateTotalPrice(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].quantity;
}
return total;
}
// ✅ 压缩后的代码(500B)
function c(t){let e=0;for(let i=0;i<t.length;i++)e+=t[i].price*t[i].quantity;return e}
// 压缩效果:
// - 减少75%的大小
// - 传输时间减少75%
// - 解析时间也减少(更少的字符)
2. 代码分割(Code Splitting)
javascript
// ❌ 不分割:所有代码打包在一起(2MB)
// main.js: 2MB
// 用户需要等待2MB下载完才能使用
// ✅ 分割:按需加载
// main.js: 200KB(核心代码)
// vendor.js: 500KB(第三方库,可以缓存)
// page1.js: 100KB(页面1的代码,按需加载)
// page2.js: 100KB(页面2的代码,按需加载)
// 使用动态导入:
const Page1 = lazy(() => import('./pages/Page1'));
const Page2 = lazy(() => import('./pages/Page2'));
// 优势:
// - 初始加载只需要200KB
// - 其他代码按需加载
// - 总传输量相同,但用户体验更好
3. Tree Shaking(树摇)
javascript
// ❌ 导入整个库(50KB)
import _ from 'lodash';
// 只使用了 _.debounce
const debouncedFn = _.debounce(fn, 300);
// ✅ 只导入需要的函数(5KB)
import debounce from 'lodash/debounce';
const debouncedFn = debounce(fn, 300);
// 优势:
// - 减少90%的代码量
// - 更快的下载和解析
4. 图片优化
javascript
// ❌ 未优化的图片(2MB)
<img src="photo.jpg" />
// ✅ 优化的图片(200KB)
// 1. 压缩质量
<img src="photo-optimized.jpg" />
// 2. 使用现代格式
<img src="photo.webp" /> // WebP格式,体积更小
// 3. 响应式图片
<img
srcset="photo-small.jpg 480w,
photo-medium.jpg 768w,
photo-large.jpg 1200w"
sizes="(max-width: 480px) 100vw, 50vw"
src="photo-medium.jpg"
/>
// 4. 懒加载
<img src="photo.jpg" loading="lazy" />
// 优势:
// - 减少90%的图片体积
// - 移动端加载更小的图片
// - 不在视口的图片延迟加载
5. 字体优化
css
/* ❌ 加载完整字体文件(500KB) */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
}
/* ✅ 只加载需要的字符(50KB) */
@font-face {
font-family: 'CustomFont';
src: url('font-subset.woff2'); /* 只包含常用字符 */
unicode-range: U+0020-007F; /* 只包含ASCII字符 */
}
/* ✅ 使用font-display优化加载 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
font-display: swap; /* 先显示备用字体,字体加载后替换 */
}
1.2.3 为什么资源大小影响性能?
传输时间计算
javascript
// 假设网速:5 Mbps(移动网络)
// 场景1:小资源(100KB)
传输时间 = 100KB × 8 ÷ 5Mbps = 0.16秒
// 场景2:大资源(1MB)
传输时间 = 1MB × 8 ÷ 5Mbps = 1.6秒
// 场景3:超大资源(10MB)
传输时间 = 10MB × 8 ÷ 5Mbps = 16秒
// 用户体验:
// - 0.16秒:几乎无感知
// - 1.6秒:可以接受
// - 16秒:用户可能离开
解析和执行时间
javascript
// JavaScript文件大小对性能的影响:
// 1. 下载时间(网络)
// 2. 解析时间(浏览器)
// 3. 执行时间(JavaScript引擎)
// 100KB的JS文件:
// - 下载:0.16秒
// - 解析:0.05秒
// - 执行:0.02秒
// 总计:0.23秒
// 1MB的JS文件:
// - 下载:1.6秒
// - 解析:0.5秒
// - 执行:0.2秒
// 总计:2.3秒
// 结论:文件越大,每个阶段都更慢
📚 第二章:数据链路层的前端优化启示
2.1 MTU限制:为什么数据包不能太大?
2.1.1 什么是MTU?
MTU(Maximum Transmission Unit)最大传输单元
数据链路层(以太网)的MTU限制:
以太网帧的最大长度:1500字节
┌─────────────────────────────────┐
│ 以太网帧结构: │
│ │
│ ┌──────────┬──────────────────┐ │
│ │ 帧头 │ 数据部分 │ │
│ │ 14字节 │ 最大1500字节 │ │
│ └──────────┴──────────────────┘ │
│ │
│ 如果数据超过1500字节怎么办? │
│ → 需要分片(Fragment) │
└─────────────────────────────────┘
为什么是1500字节?
历史原因:
- 早期网络设备的内存限制
- 平衡传输效率和错误恢复
- 成为以太网标准
实际影响:
- 每个数据包最大1500字节
- 超过需要分片
- 分片会增加延迟和开销
2.1.2 IP分片对性能的影响
IP分片过程
假设要传输3000字节的数据:
┌─────────────────────────────────┐
│ 原始数据:3000字节 │
│ │
│ 需要分成3个包: │
│ │
│ 包1:1500字节(包含分片信息) │
│ 包2:1500字节(包含分片信息) │
│ 包3:剩余数据(包含分片信息) │
│ │
│ 问题: │
│ 1. 每个包都需要单独传输 │
│ 2. 如果包2丢失,整个数据重传 │
│ 3. 接收端需要等待所有包到达 │
│ 4. 增加延迟和开销 │
└─────────────────────────────────┘
前端如何避免IP分片?
1. 控制单个请求的数据大小
javascript
// ❌ 可能触发分片(如果数据很大)
fetch('/api/data', {
method: 'POST',
body: JSON.stringify({
// 如果这个对象序列化后超过1500字节
// 可能会被分片传输
items: hugeArray // 假设1000个元素
})
})
// ✅ 优化:分批传输
async function sendDataInBatches(items) {
const batchSize = 100; // 每批100个
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
// 分批发送,每批数据量小,不会触发分片
for (const batch of batches) {
await fetch('/api/data', {
method: 'POST',
body: JSON.stringify({ items: batch })
});
}
}
2. 使用压缩减少数据大小
javascript
// ❌ 未压缩的JSON(可能超过1500字节)
const data = {
description: "这是一个很长的描述..." // 假设2000字符
};
fetch('/api/data', {
method: 'POST',
body: JSON.stringify(data) // 可能触发分片
})
// ✅ 使用压缩
async function sendCompressedData(data) {
// 使用gzip压缩
const jsonString = JSON.stringify(data);
const compressed = await compress(jsonString); // 压缩后可能只有500字节
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Encoding': 'gzip'
},
body: compressed // 压缩后不会触发分片
})
}
// 注意:现代浏览器和服务器通常自动处理压缩
// 但了解原理有助于优化
3. 分页加载数据
javascript
// ❌ 一次性加载所有数据(可能很大)
fetch('/api/posts') // 返回1000条数据,可能触发分片
// ✅ 分页加载
async function loadPosts(page = 1, pageSize = 20) {
const response = await fetch(`/api/posts?page=${page}&size=${pageSize}`);
return response.json();
}
// 优势:
// - 每页数据量小(不会触发分片)
// - 用户体验更好(渐进式加载)
// - 减少服务器压力
2.1.3 实际优化案例
案例1:大文件上传
javascript
// ❌ 直接上传大文件(会触发大量分片)
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file); // 假设10MB文件
fetch('/api/upload', {
method: 'POST',
body: formData
});
// 10MB ÷ 1500字节 ≈ 6667个包
// 每个包都需要单独传输和确认
}
// ✅ 分块上传
async function uploadFileInChunks(file) {
const chunkSize = 1 * 1024 * 1024; // 1MB每块
const totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
// 显示进度
updateProgress((i + 1) / totalChunks * 100);
}
}
// 优势:
// - 每个块独立传输(失败可以重试单个块)
// - 可以显示上传进度
// - 不会因为网络问题导致整个文件重传
案例2:WebSocket消息大小
javascript
// ❌ 发送大消息(可能触发分片)
const ws = new WebSocket('ws://example.com');
ws.send(JSON.stringify({
// 假设这个对象序列化后很大
data: hugeObject
}));
// ✅ 控制消息大小
function sendLargeData(ws, data) {
const jsonString = JSON.stringify(data);
const maxChunkSize = 1000; // 每块1000字节(小于1500)
if (jsonString.length <= maxChunkSize) {
// 小消息直接发送
ws.send(jsonString);
} else {
// 大消息分块发送
const chunks = [];
for (let i = 0; i < jsonString.length; i += maxChunkSize) {
chunks.push(jsonString.slice(i, i + maxChunkSize));
}
// 发送分块信息
ws.send(JSON.stringify({
type: 'chunked',
total: chunks.length,
chunks: chunks
}));
}
}
2.2 数据帧的封装开销:为什么请求头很重要?
2.2.1 数据帧的结构
以太网帧结构:
┌─────────────────────────────────────┐
│ 帧头(14字节) │
│ ├─ 目标MAC地址(6字节) │
│ ├─ 源MAC地址(6字节) │
│ └─ 协议类型(2字节) │
├─────────────────────────────────────┤
│ IP头部(20字节) │
│ ├─ 源IP地址(4字节) │
│ ├─ 目标IP地址(4字节) │
│ └─ 其他信息(12字节) │
├─────────────────────────────────────┤
│ TCP头部(20字节) │
│ ├─ 源端口(2字节) │
│ ├─ 目标端口(2字节) │
│ └─ 其他信息(16字节) │
├─────────────────────────────────────┤
│ HTTP头部(可变,通常几百字节) │
│ ├─ Host: example.com │
│ ├─ User-Agent: ... │
│ ├─ Cookie: ... │
│ └─ 其他头部... │
├─────────────────────────────────────┤
│ 数据部分(实际要传输的内容) │
└─────────────────────────────────────┘
总开销:
- 帧头:14字节
- IP头:20字节
- TCP头:20字节
- HTTP头:通常200-1000字节
- 总计:约250-1050字节的开销
2.2.2 前端如何减少头部开销?
1. 减少Cookie大小
javascript
// ❌ Cookie太大(增加每个请求的开销)
document.cookie = "user=very-long-user-id-string-1234567890";
document.cookie = "session=another-very-long-session-token-abcdefghijklmnop";
document.cookie = "preferences=large-json-object-with-many-settings";
// 每个请求都会携带这些Cookie
// 如果Cookie总共2KB,每个请求都增加2KB开销
// ✅ 优化Cookie
// 1. 只存储必要的信息
document.cookie = "uid=123"; // 只存储ID
document.cookie = "sid=abc"; // 只存储session ID
// 2. 使用HttpOnly和Secure
// 3. 使用SameSite防止CSRF
// 4. 考虑使用Token代替Cookie
// 使用Token(存储在内存,不随每个请求发送)
localStorage.setItem('token', 'abc123');
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
2. 减少HTTP头部
javascript
// ❌ 不必要的头部
fetch('/api/data', {
headers: {
'X-Custom-Header-1': 'value1',
'X-Custom-Header-2': 'value2',
'X-Custom-Header-3': 'value3',
'X-Unnecessary-Header': 'not-needed'
}
});
// ✅ 只保留必要的头部
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
// 移除不必要的自定义头部
}
});
3. 使用HTTP/2头部压缩
javascript
// HTTP/1.1:头部不压缩
// 每个请求都发送完整的头部
// HTTP/2:使用HPACK压缩
// - 第一次请求:发送完整头部
// - 后续请求:只发送变化的头部
// - 大幅减少头部大小
// 前端不需要特殊处理
// 但使用HTTP/2服务器就能自动获得这个优化
4. 合并请求减少头部开销
javascript
// ❌ 多个小请求(每个都有头部开销)
fetch('/api/user'); // 头部开销:500字节
fetch('/api/posts'); // 头部开销:500字节
fetch('/api/comments'); // 头部开销:500字节
// 总开销:1500字节
// ✅ 合并请求(只有一个头部开销)
fetch('/api/batch', {
method: 'POST',
body: JSON.stringify({
queries: [
{ path: '/user' },
{ path: '/posts' },
{ path: '/comments' }
]
})
});
// 总开销:500字节(节省1000字节)
// 或者使用GraphQL
fetch('/graphql', {
method: 'POST',
body: JSON.stringify({
query: `
{
user { name }
posts { title }
comments { content }
}
`
})
});
// 一个请求获取所有数据
📚 第三章:网络层的前端优化策略
3.1 IP地址和路由:CDN的原理
3.1.1 IP地址的作用
网络层(IP层)的作用:
┌─────────────────────────────────┐
│ 主机A (IP: 192.168.1.10) │
│ │ │
│ │ 数据包 │
│ │ 目标IP: 203.0.113.5 │
│ ↓ │
│ 路由器1 │
│ │ │
│ │ 根据目标IP路由 │
│ ↓ │
│ 路由器2 │
│ │ │
│ ↓ │
│ 主机B (IP: 203.0.113.5) │
└─────────────────────────────────┘
IP地址的作用:
- 标识网络中的每台设备
- 路由器根据IP地址决定数据包的路由路径
- 距离越远,经过的路由器越多,延迟越高
3.1.2 CDN如何利用IP路由优化性能?
CDN(Content Delivery Network)内容分发网络
没有CDN的情况:
用户(北京) → 服务器(美国)
距离:10000公里
延迟:200ms
经过的路由器:20个
┌─────────┐ ┌─────────┐
│ 用户 │ ────200ms────→ │ 服务器 │
│ 北京 │ │ 美国 │
└─────────┘ └─────────┘
有CDN的情况:
用户(北京) → CDN节点(北京)
距离:10公里
延迟:10ms
经过的路由器:2个
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 用户 │ → │ CDN节点 │ ← │ 源服务器│
│ 北京 │ │ 北京 │ │ 美国 │
└─────────┘ └─────────┘ └─────────┘
10ms 第一次200ms
之后从缓存读取
优势:
- 延迟从200ms降到10ms(20倍提升)
- 减少网络拥塞
- 提高用户体验
前端如何使用CDN?
html
<!-- ❌ 从源服务器加载 -->
<script src="https://example.com/js/app.js"></script>
<link rel="stylesheet" href="https://example.com/css/style.css">
<!-- ✅ 从CDN加载 -->
<script src="https://cdn.example.com/js/app.js"></script>
<link rel="stylesheet" href="https://cdn.example.com/css/style.css">
<!-- 或者使用公共CDN -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">
CDN的工作原理
javascript
// 1. DNS解析
// 用户请求 cdn.example.com
// DNS返回距离用户最近的CDN节点IP
// 2. 请求路由
// 浏览器连接到最近的CDN节点
// 而不是连接到源服务器
// 3. 缓存机制
// CDN节点缓存静态资源
// 第一次请求:CDN从源服务器获取(慢)
// 后续请求:CDN直接返回缓存(快)
// 前端配置示例(Webpack)
module.exports = {
output: {
publicPath: 'https://cdn.example.com/', // CDN地址
filename: '[name].[contenthash].js'
}
};
CDN的最佳实践
javascript
// 1. 静态资源使用CDN
// - JS、CSS、图片、字体等
// - 这些资源不经常变化
// 2. 动态内容不使用CDN
// - API请求
// - 用户数据
// - 这些内容需要实时性
// 3. 使用多个CDN提供商
// - 提高可用性
// - 降低单点故障风险
// 4. 配置缓存策略
// HTML: 不缓存或短时间缓存
// JS/CSS: 长时间缓存(使用版本号)
// 图片: 中等时间缓存
3.2 IP分片:为什么需要避免?
3.2.1 IP分片的代价
IP分片的问题:
1. 性能损失
- 每个分片都需要单独传输
- 如果任何一个分片丢失,整个数据包需要重传
- 接收端需要等待所有分片到达才能重组
2. 延迟增加
- 分片需要时间
- 重组需要时间
- 如果分片乱序到达,需要等待
3. 资源消耗
- 路由器需要处理分片
- 接收端需要缓存分片
- 增加内存和CPU使用
3.2.2 前端如何避免IP分片?
1. 控制请求体大小
javascript
// ❌ 可能触发分片
const largeData = {
items: new Array(10000).fill({ /* 大量数据 */ })
};
fetch('/api/data', {
method: 'POST',
body: JSON.stringify(largeData) // 可能超过MTU
});
// ✅ 分批发送
async function sendLargeData(data) {
const chunkSize = 1000; // 每批1000条
const chunks = [];
for (let i = 0; i < data.items.length; i += chunkSize) {
chunks.push(data.items.slice(i, i + chunkSize));
}
for (const chunk of chunks) {
await fetch('/api/data', {
method: 'POST',
body: JSON.stringify({ items: chunk })
});
}
}
2. 使用压缩
javascript
// 压缩可以大幅减少数据大小
// 从而避免分片
// 浏览器自动处理(如果服务器支持)
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, deflate, br' // 浏览器自动添加
},
body: JSON.stringify(data)
});
// 服务器应该配置压缩
// Express示例:
const compression = require('compression');
app.use(compression());
3. 优化数据结构
javascript
// ❌ 冗余的数据结构
const data = {
user: {
id: 1,
name: "John",
email: "john@example.com",
// ... 很多字段
},
posts: [
{
id: 1,
userId: 1, // 冗余:user已经包含id
title: "...",
// ...
}
]
};
// ✅ 精简的数据结构
const data = {
user: {
id: 1,
name: "John",
email: "john@example.com"
},
posts: [
{
id: 1,
// 不包含userId(可以从user.id获取)
title: "..."
}
]
};
// 或者使用更紧凑的格式
const data = {
u: { i: 1, n: "John", e: "john@example.com" },
p: [{ i: 1, t: "..." }]
};
📚 第四章:传输层的前端优化
4.1 TCP连接:为什么连接复用很重要?
4.1.1 TCP三次握手
TCP建立连接的过程(三次握手):
客户端 服务器
│ │
│ ────SYN────→ │
│ │
│ ←──SYN+ACK─── │
│ │
│ ────ACK────→ │
│ │
│ 连接建立完成 │
时间消耗:
- 每次握手需要1个RTT(往返时间)
- 三次握手 = 1.5个RTT
- 如果RTT = 50ms,建立连接需要75ms
前端如何减少连接建立时间?
javascript
// ❌ 每次请求都建立新连接
for (let i = 0; i < 10; i++) {
fetch('/api/data' + i); // 每个请求都需要三次握手
}
// 总时间:10 × 75ms = 750ms(仅连接建立)
// ✅ 连接复用(HTTP Keep-Alive)
// 浏览器自动复用TCP连接
// 第一个请求:建立连接(75ms)
// 后续请求:复用连接(0ms)
fetch('/api/data1'); // 建立连接
fetch('/api/data2'); // 复用连接
fetch('/api/data3'); // 复用连接
// 总时间:75ms(仅第一个请求)
// HTTP/1.1默认启用Keep-Alive
// 但每个域名通常只保持6个连接
4.1.2 TCP慢启动
TCP慢启动(Slow Start):
连接刚建立时,传输速度很慢
然后逐渐增加,直到达到稳定速度
时间 →
速度
│ ╱╲
│ ╱ ╲
│ ╱ ╲─────── 稳定速度
│ ╱ ╲
│ ╱ ╲
│╱ ╲
└──────────────→
问题:
- 小文件传输时,速度还没提升就结束了
- 浪费了连接的性能潜力
前端如何利用TCP慢启动?
javascript
// ❌ 多个小请求(每个都经历慢启动)
fetch('/api/user'); // 慢启动阶段
fetch('/api/posts'); // 慢启动阶段
fetch('/api/comments'); // 慢启动阶段
// ✅ 合并请求(只经历一次慢启动)
fetch('/api/batch', {
method: 'POST',
body: JSON.stringify({
user: true,
posts: true,
comments: true
})
});
// 一个请求,一次慢启动,但传输更多数据
// ✅ 使用HTTP/2(多路复用,共享连接)
// 所有请求共享一个TCP连接
// 连接建立后,所有请求都能利用稳定速度
4.1.3 TCP连接复用优化
javascript
// 1. 域名合并
// ❌ 多个域名(每个都需要建立连接)
fetch('https://api1.example.com/data');
fetch('https://api2.example.com/data');
fetch('https://cdn.example.com/image.jpg');
// ✅ 合并域名(共享连接)
fetch('https://api.example.com/data1');
fetch('https://api.example.com/data2');
fetch('https://api.example.com/image.jpg');
// 2. 使用HTTP/2
// HTTP/2的多路复用可以更好地利用TCP连接
// 一个连接可以同时处理多个请求
// 3. 预连接(Preconnect)
// 提前建立连接,减少首次请求延迟
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
// 4. 连接池管理
// 现代浏览器自动管理连接池
// 但需要注意:
// - 不要创建太多不同的域名
// - 合理使用子域名(平衡并发和缓存)
4.2 TCP拥塞控制:为什么需要控制请求频率?
4.2.1 TCP拥塞控制原理
TCP拥塞控制:
当网络拥塞时,TCP会降低传输速度
当网络通畅时,TCP会提高传输速度
网络状态:
│
│ 正常 ────→ 拥塞 ────→ 正常
│ ↑ │
│ └──────── 恢复 ──────────┘
TCP响应:
│
│ 快速 ────→ 慢速 ────→ 快速
│ ↑ │
│ └──────── 恢复 ──────────┘
4.2.2 前端如何避免触发拥塞控制?
1. 请求节流(Throttling)
javascript
// ❌ 同时发送大量请求(可能触发拥塞)
for (let i = 0; i < 100; i++) {
fetch(`/api/data${i}`);
}
// 100个请求同时发送,可能导致网络拥塞
// ✅ 请求节流
async function fetchWithThrottle(urls, concurrency = 5) {
const results = [];
for (let i = 0; i < urls.length; i += concurrency) {
const batch = urls.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map(url => fetch(url))
);
results.push(...batchResults);
}
return results;
}
// 使用:最多同时5个请求
const urls = Array.from({ length: 100 }, (_, i) => `/api/data${i}`);
fetchWithThrottle(urls, 5);
2. 请求去重(Deduplication)
javascript
// ❌ 重复请求(浪费带宽)
function loadUser() {
fetch('/api/user'); // 请求1
}
function loadProfile() {
fetch('/api/user'); // 请求2(重复)
}
// ✅ 请求去重
const pendingRequests = new Map();
function fetchWithDedup(url) {
if (pendingRequests.has(url)) {
return pendingRequests.get(url);
}
const promise = fetch(url)
.then(response => {
pendingRequests.delete(url);
return response;
})
.catch(error => {
pendingRequests.delete(url);
throw error;
});
pendingRequests.set(url, promise);
return promise;
}
// 使用
function loadUser() {
fetchWithDedup('/api/user');
}
function loadProfile() {
fetchWithDedup('/api/user'); // 复用第一个请求
}
3. 请求优先级
javascript
// 根据重要性设置请求优先级
// 重要请求优先,次要请求延迟
class RequestQueue {
constructor() {
this.highPriority = [];
this.lowPriority = [];
this.processing = false;
}
add(url, priority = 'low') {
if (priority === 'high') {
this.highPriority.push(url);
} else {
this.lowPriority.push(url);
}
this.process();
}
async process() {
if (this.processing) return;
this.processing = true;
// 先处理高优先级
while (this.highPriority.length > 0) {
const url = this.highPriority.shift();
await fetch(url);
}
// 再处理低优先级
while (this.lowPriority.length > 0) {
const url = this.lowPriority.shift();
await fetch(url);
}
this.processing = false;
}
}
// 使用
const queue = new RequestQueue();
queue.add('/api/critical-data', 'high'); // 优先
queue.add('/api/analytics', 'low'); // 延迟
📚 第五章:应用层的前端优化实战
5.1 HTTP协议优化
5.1.1 HTTP缓存策略
缓存的作用
没有缓存:
用户请求 → 服务器 → 返回数据
每次都需要网络传输
有缓存:
第一次:用户请求 → 服务器 → 返回数据(缓存)
第二次:用户请求 → 浏览器缓存 → 直接返回
不需要网络传输,速度极快
前端缓存策略
javascript
// 1. 强缓存(Cache-Control)
// 服务器设置响应头
// Cache-Control: max-age=31536000 // 1年
// 前端使用:
// - 静态资源(JS/CSS/图片)使用强缓存
// - 文件名包含hash,内容变化时文件名变化
// - 浏览器自动处理
// 2. 协商缓存(ETag/Last-Modified)
// 第一次请求:服务器返回ETag
// 第二次请求:浏览器发送If-None-Match
// 如果未变化,服务器返回304,浏览器使用缓存
// 3. Service Worker缓存
// 更细粒度的缓存控制
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中,直接返回
if (response) {
return response;
}
// 缓存未命中,请求网络
return fetch(event.request).then(response => {
// 缓存响应
const responseToCache = response.clone();
caches.open('v1').then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
5.1.2 HTTP压缩
javascript
// 服务器启用压缩
// Express示例:
const compression = require('compression');
app.use(compression({
level: 6, // 压缩级别(1-9)
filter: (req, res) => {
// 只压缩文本类型
return /text|javascript|json|css/.test(res.getHeader('Content-Type'));
}
}));
// 前端自动处理
// 浏览器发送请求时自动添加:
// Accept-Encoding: gzip, deflate, br
// 压缩效果:
// 原始:100KB
// 压缩后:20KB(减少80%)
// 传输时间减少80%
5.1.3 HTTP/2和HTTP/3
HTTP/2的优势
javascript
// 1. 多路复用
// 一个TCP连接可以同时处理多个请求
// 2. 头部压缩(HPACK)
// 大幅减少头部大小
// 3. 服务器推送
// 服务器可以主动推送资源
// 前端使用:
// - 使用支持HTTP/2的服务器
// - 配置SSL(HTTP/2需要HTTPS)
// - 浏览器自动使用HTTP/2(如果服务器支持)
HTTP/3的优势
javascript
// HTTP/3基于UDP(QUIC协议)
// 优势:
// 1. 更快的连接建立(0-RTT)
// 2. 更好的拥塞控制
// 3. 连接迁移(切换网络不断线)
// 前端使用:
// - 使用支持HTTP/3的服务器
// - 浏览器自动使用(Chrome、Edge已支持)
5.2 实际优化案例
5.2.1 图片优化
javascript
// 1. 使用现代格式
// WebP:比JPEG小30%,质量相同
// AVIF:比JPEG小50%,质量更好
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
// 2. 响应式图片
<img
srcset="small.jpg 480w,
medium.jpg 768w,
large.jpg 1200w"
sizes="(max-width: 480px) 100vw, 50vw"
src="medium.jpg"
/>
// 3. 懒加载
<img src="image.jpg" loading="lazy" />
// 4. 使用CDN
<img src="https://cdn.example.com/image.jpg" />
5.2.2 JavaScript优化
javascript
// 1. 代码分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// 2. Tree Shaking
import { debounce } from 'lodash-es'; // 只导入需要的
// 3. 压缩
// 使用构建工具自动压缩
// 4. 使用现代语法
// 使用ES6+,让浏览器更好地优化
// 5. 避免大型库
// 使用轻量级替代方案
5.2.3 网络请求优化
javascript
// 1. 请求合并
async function loadPageData() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// 使用HTTP/2时,可以并行请求
}
// 2. 请求去重
const requestCache = new Map();
function fetchWithCache(url) {
if (requestCache.has(url)) {
return requestCache.get(url);
}
const promise = fetch(url)
.then(res => res.json())
.finally(() => requestCache.delete(url));
requestCache.set(url, promise);
return promise;
}
// 3. 请求重试
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
// 4. 请求取消
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal });
// 取消请求
controller.abort();
📚 第六章:综合优化方案
6.1 性能优化检查清单
✅ 网络层优化
□ 使用CDN加速静态资源
□ 减少DNS查询(合并域名)
□ 使用HTTP/2或HTTP/3
□ 启用Gzip/Brotli压缩
□ 避免IP分片(控制数据包大小)
✅ 传输层优化
□ 复用TCP连接(Keep-Alive)
□ 减少连接建立(域名合并)
□ 使用HTTP/2多路复用
□ 控制请求频率(节流)
✅ 应用层优化
□ 启用HTTP缓存
□ 使用Service Worker
□ 合并请求(减少头部开销)
□ 减少Cookie大小
□ 使用资源优先级
✅ 资源优化
□ 压缩JavaScript/CSS
□ 压缩图片(WebP/AVIF)
□ 代码分割和懒加载
□ Tree Shaking
□ 字体子集化
✅ 请求优化
□ 请求去重
□ 请求节流
□ 请求优先级
□ 请求重试机制
□ 请求取消机制
6.2 性能监控
javascript
// 1. 使用Performance API
const perfData = performance.getEntriesByType('resource');
perfData.forEach(resource => {
console.log({
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType
});
});
// 2. 监控网络请求
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'resource') {
console.log('Resource:', entry.name, entry.duration);
}
}
});
observer.observe({ entryTypes: ['resource'] });
// 3. 监控Web Vitals
// LCP (Largest Contentful Paint)
// FID (First Input Delay)
// CLS (Cumulative Layout Shift)
// 使用web-vitals库
import { getLCP, getFID, getCLS } from 'web-vitals';
getLCP(console.log);
getFID(console.log);
getCLS(console.log);
6.3 优化效果评估
优化前:
- 首屏加载时间:3秒
- 总资源大小:5MB
- 请求数量:50个
- 网络传输时间:2.5秒
优化后:
- 首屏加载时间:1秒(提升66%)
- 总资源大小:2MB(减少60%)
- 请求数量:20个(减少60%)
- 网络传输时间:0.8秒(提升68%)
关键指标:
- Time to First Byte (TTFB)
- First Contentful Paint (FCP)
- Largest Contentful Paint (LCP)
- Total Blocking Time (TBT)
🎓 总结
核心要点回顾
-
物理层的全双工原理 → HTTP/2多路复用,一个连接同时处理多个请求
-
数据链路层的MTU限制 → 控制数据包大小,避免分片,减少延迟
-
网络层的IP路由 → 使用CDN,将资源放在离用户更近的地方
-
传输层的TCP特性 → 连接复用、慢启动优化、拥塞控制
-
应用层的HTTP协议 → 缓存、压缩、HTTP/2/3升级
优化原则
1. 减少数据传输量
- 压缩资源
- 精简数据结构
- 使用现代格式
2. 减少网络往返
- 合并请求
- 使用缓存
- HTTP/2多路复用
3. 减少延迟
- 使用CDN
- 预连接
- 减少DNS查询
4. 提高并发
- HTTP/2
- 请求优先级
- 资源预加载
实践建议
1. 从底层原理理解优化
- 知道为什么优化有效
- 能够选择最合适的优化方案
2. 测量和监控
- 优化前测量基准
- 优化后验证效果
- 持续监控性能
3. 平衡优化和复杂度
- 不要过度优化
- 优先优化影响最大的部分
- 保持代码可维护性
4. 关注用户体验
- 首屏加载时间
- 交互响应时间
- 网络差的情况下的表现
📖 延伸阅读
希望这篇指南能帮助你深入理解网络体系结构,并将其应用到前端性能优化中! 🚀