
你的代码可能还在用HTTP/1.1时代的思维写Fetch,而HTTP/2早已改变了游戏规则。
去年我在做性能优化时,发现了一个很有意思的现象:同样的代码,在HTTP/2环境下比HTTP/1.1快了3倍,但很多开发者还在用老方法写Fetch。这不是框架的问题,也不是服务器的锅,而是我们的代码思维没有跟上协议的进化。
今天,我们就来深挖HTTP/2到底改变了什么,以及如何正确使用Fetch API来榨干每一分性能。
HTTP/1.1时代:每个资源都是独立户口
想象一下你在火车站排队买票:
HTTP/1.1 的世界:
┌─────────────┐
│ 浏览器 │
└─────┬───────┘
│ 请求CSS
├─────────────────> ┌─────────┐
│ <─────────────────┤ 服务器 │
│ 响应CSS └─────────┘
│
│ 请求JS (等CSS下完才能发)
├─────────────────>
│ <─────────────────
│ 响应JS
│
│ 请求图片 (继续等...)
├─────────────────>
│ <─────────────────
│ 响应图片
在HTTP/1.1时代,每个资源都需要单独排队、单独建立连接、单独握手。这就像你去银行办业务,明明有5件事要办,却要排5次队,每次都要重新取号。
这种“队头阻塞”(Head-of-Line Blocking)是HTTP/1.1的致命伤:
- 一个请求卡住,后面全部等待
- 浏览器限制每个域名最多6个并发连接
- 大量的TCP握手和SSL握手开销
所以那个时代的优化手段是什么?合并、合并、再合并:
- 把所有JS打包成一个bundle.js
- 雪碧图(CSS Sprites)把图片合并
- 域名分片来突破6个连接的限制
但这些“脏活累活”在HTTP/2时代,全成了反优化。
HTTP/2革命:一条高速公路搞定所有请求
HTTP/2带来了多路复用(Multiplexing)技术,彻底改变了游戏规则:
HTTP/2 的世界:
┌─────────────┐
│ 浏览器 │
└─────┬───────┘
│ 一条TCP连接
├════════════════════> ┌─────────┐
║ Stream 1: CSS请求 │ │
║ Stream 2: JS请求 │ 服务器 │
║ Stream 3: 图片请求 │ │
║ │ │
║ <────── CSS响应 │ │
║ <────── 图片响应 │ │
║ <────── JS响应 └─────────┘
║ (乱序返回,互不阻塞)
这就像从“单车道”升级到了“多车道高速公路”:
- 一个TCP连接承载所有请求
- 请求和响应可以并行、乱序传输
- 没有队头阻塞,快的先走
- 自动压缩HTTP头,减少冗余数据
听起来很美好,对吧?但问题来了:你的代码准备好了吗?
错误示范:还在串行请求?你在浪费HTTP/2
我见过太多开发者写出这样的代码:
// ❌ 错误示范:串行请求
async function loadDashboard() {
// 等用户数据回来
const user = await fetch('/api/user').then(r => r.json());
// 再请求通知数据
const notifications = await fetch('/api/notifications').then(r => r.json());
// 最后请求统计数据
const stats = await fetch('/api/stats').then(r => r.json());
console.log(user, notifications, stats);
}
这个代码有什么问题?
让我们算一笔账:假设每个API响应时间是100ms,这个函数总耗时是300ms。
但HTTP/2支持并行啊!为什么要让它们排队?
这就像你去麦当劳点餐,明明可以一次性说“我要汉堡、可乐、薯条”,你却分三次排队,每次只点一样。
正确姿势:拥抱并行,释放HTTP/2的真正威力
改成并行请求,性能立刻起飞:
// ✅ 正确示范:并行请求
async function loadDashboard() {
const [user, notifications, stats] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/notifications').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]);
console.log(user, notifications, stats);
}
使用 Promise.all,三个请求同时发出,通过HTTP/2的多路复用在一条连接上并行传输。
总耗时从300ms降到100ms,性能提升3倍!
这个优化在真实项目中的效果更明显:
- 字节的某个管理后台,首屏加载从2.3s降到0.8s
- 阿里云控制台,并行请求改造后API耗时减少60%
- 腾讯会议,数据面板渲染速度提升2倍
深入原理:HTTP/2多路复用到底怎么工作?
让我们从底层看看HTTP/2的魔法:
1. 二进制分帧层(Binary Framing Layer)
HTTP/2把每个请求/响应拆分成帧(Frame),每个帧都带有Stream ID:
HTTP/1.1 传输:
文本格式,一个响应是连续的字节流
GET /api/user HTTP/1.1
Host: example.com
...
HTTP/2 传输:
二进制格式,拆成多个帧
┌──────────┬──────────┬──────────┐
│Frame(S1) │Frame(S2) │Frame(S1) │
│HEADERS │DATA │DATA │
└──────────┴──────────┴──────────┘
2. Stream优先级(Stream Priority)
你可以告诉浏览器哪些资源更重要:
// 高优先级的关键CSS
fetch('/critical.css', { priority: 'high' });
// 低优先级的统计脚本
fetch('/analytics.js', { priority: 'low' });
浏览器会根据优先级调度资源加载,确保关键路径优先。
3. 服务器推送(Server Push)
服务器可以主动推送资源,不需要等浏览器请求:
浏览器请求 index.html
↓
服务器响应 index.html
同时主动推送:
- style.css
- script.js
- logo.png
这个特性虽然强大,但使用要谨慎,推送不当会适得其反。
实战技巧:5个立刻能用的HTTP/2优化模式
技巧1:初始化数据并行加载
// 应用启动时,并行加载所有初始化数据
async function initializeApp() {
const [
userProfile,
appConfig,
permissions,
menuItems
] = await Promise.all([
fetch('/api/user/profile').then(r => r.json()),
fetch('/api/config').then(r => r.json()),
fetch('/api/permissions').then(r => r.json()),
fetch('/api/menu').then(r => r.json())
]);
return { userProfile, appConfig, permissions, menuItems };
}
收益:从串行4×100ms=400ms → 并行100ms,节省75%时间
技巧2:列表详情双重加载
// 同时加载列表和第一条详情
async function loadListWithFirstDetail() {
const listPromise = fetch('/api/articles').then(r => r.json());
const list = await listPromise;
// 有了列表,立即并行加载第一条详情和其他数据
const [firstDetail, categories] = await Promise.all([
fetch(`/api/articles/${list[0].id}`).then(r => r.json()),
fetch('/api/categories').then(r => r.json())
]);
return { list, firstDetail, categories };
}
收益:用户看到列表的同时,详情已经在加载,体验丝滑
技巧3:分块加载大数据
// 对于大数据集,先加载核心数据,再加载详细数据
async function loadUserDashboard(userId) {
// 第一波:核心数据(快速展示)
const [summary, recentActivity] = await Promise.all([
fetch(`/api/user/${userId}/summary`).then(r => r.json()),
fetch(`/api/user/${userId}/recent`).then(r => r.json())
]);
// 渲染核心UI
renderCoreDashboard(summary, recentActivity);
// 第二波:详细数据(后台加载)
const [fullHistory, analytics] = await Promise.all([
fetch(`/api/user/${userId}/history`).then(r => r.json()),
fetch(`/api/user/${userId}/analytics`).then(r => r.json())
]);
// 补充详细信息
renderDetailedDashboard(fullHistory, analytics);
}
收益:渐进式渲染,首屏更快,体验更好
技巧4:流式处理大响应
HTTP/2配合Fetch的 ReadableStream,可以边下载边处理:
async function streamLargeJSON() {
const response = await fetch('/api/large-dataset');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码chunk
buffer += decoder.decode(value, { stream: true });
// 尝试处理完整的JSON对象
// (实际项目中可以用ndjson格式,每行一个JSON)
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
lines.forEach(line => {
if (line.trim()) {
const item = JSON.parse(line);
// 立即处理每一条数据
processItem(item);
}
});
}
}
收益:不用等整个响应下载完,边下载边渲染,降低首字节延迟
技巧5:智能预加载
// 鼠标悬停时预加载详情
function setupSmartPrefetch() {
document.querySelectorAll('.article-item').forEach(item => {
item.addEventListener('mouseenter', () => {
const articleId = item.dataset.id;
// 预加载详情到缓存
fetch(`/api/articles/${articleId}`)
.then(r => r.json())
.then(data => {
// 存入缓存,点击时秒开
cache.set(articleId, data);
});
});
});
}
收益:用户点击前就开始加载,点击后秒开,极致体验
配合Cache API:让HTTP/2如虎添翼
HTTP/2让网络更快,Cache API让重复访问更快:
// 预缓存关键资源
async function precacheAssets() {
const cache = await caches.open('app-v1');
// 并行缓存多个资源
await Promise.all([
cache.add('/styles/main.css'),
cache.add('/scripts/app.js'),
cache.add('/images/logo.png'),
cache.add('/data/config.json')
]);
console.log('关键资源已缓存');
}
// 优先从缓存读取,失败才走网络
async function fetchWithCache(url) {
const cache = await caches.open('app-v1');
const cached = await cache.match(url);
if (cached) {
console.log('从缓存加载:', url);
return cached;
}
console.log('从网络加载:', url);
const response = await fetch(url);
// 缓存新资源
cache.put(url, response.clone());
return response;
}
组合拳:
- HTTP/2多路复用让首次加载快
- Cache API让后续访问快
- 配合Service Worker,实现离线可用
常见误区:HTTP/2不是万能药
误区1:域名分片还有用吗?
答案:反优化!
HTTP/1.1时代,我们用域名分片突破6个连接限制:
// HTTP/1.1优化(现在不要这样做)
cdn1.example.com/a.js
cdn2.example.com/b.js
cdn3.example.com/c.js
但在HTTP/2时代,一个域名一个连接是最优解。多域名意味着:
- 多次DNS查询
- 多次TCP握手
- 多次SSL握手
- 无法共享HTTP/2连接
结论:HTTP/2时代,统一域名,让多路复用发挥最大效能。
误区2:资源合并还需要吗?
答案:看情况!
HTTP/2减少了合并的必要性,但不是完全不需要:
需要合并的情况:
- 小图标 → 雪碧图或SVG Sprite(减少请求数)
- 内联CSS → Critical CSS内联(加快首屏)
- 核心JS → 打包(避免过度拆分)
可以拆分的情况:
- 大型库 → 按需加载(tree-shaking)
- 业务代码 → 路由懒加载(代码分割)
- 样式 → 按页面拆分(减少首屏体积)
误区3:HTTP/2会自动让一切变快?
答案:需要配合正确的代码!
HTTP/2是基础设施,但你的代码决定了能否发挥它的能力:
// HTTP/2再快,这样写还是慢
for (let i = 0; i < 10; i++) {
const data = await fetch(`/api/item/${i}`).then(r => r.json());
processData(data); // 一个一个处理,太慢!
}
// 并行才是正道
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(fetch(`/api/item/${i}`).then(r => r.json()));
}
const results = await Promise.all(promises);
results.forEach(processData); // 一起处理
浏览器兼容性:HTTP/2普及率已经很高
截至2024年,HTTP/2的支持率已经非常高:
- Chrome 41+ (2015年)
- Firefox 36+ (2015年)
- Safari 9+ (2015年)
- Edge 12+ (2015年)
全球浏览器HTTP/2支持率超过97%,你可以放心使用。
检测方法:
// 检测当前请求是否使用HTTP/2
fetch('/api/test')
.then(response => {
console.log('协议:', response.headers.get(':protocol'));
// HTTP/2 会显示 "h2"
});
真实案例:腾讯文档的HTTP/2优化之路
腾讯文档团队分享过他们的HTTP/2优化经验:
优化前:
- 文档编辑器启动:50+个串行API请求
- 首屏时间:3.2秒
- 大量资源合并,bundle体积2.5MB
优化后:
- 改为并行请求,分组加载
- 首屏时间:1.1秒(降低65%)
- 按需加载,首屏bundle降到800KB
- HTTP/2多路复用,请求数从50降到15
关键改进:
- 核心数据并行加载
- 非核心功能延迟加载
- 利用HTTP/2拆分大bundle
- 配合CDN和HTTP/2服务器推送
检查清单:你的项目HTTP/2优化了吗?
对照这个清单,看看你的项目还有多少优化空间:
□ 服务器已启用HTTP/2
□ 使用Promise.all并行加载初始化数据
□ 移除了域名分片(统一到一个域名)
□ 资源拆分适度,避免过度合并
□ 关键路径使用流式加载
□ 配合Cache API做好缓存策略
□ 利用Resource Hints预加载资源
□ 监控HTTP/2连接状态和性能
总结:HTTP/2改变的不只是协议,更是思维
从HTTP/1.1到HTTP/2,不仅是协议升级,更是开发思维的转变:
HTTP/1.1思维:
- 减少请求数量(合并、合并、合并)
- 域名分片突破连接限制
- 担心并发请求拖垮服务器
HTTP/2思维:
- 拥抱并行,让浏览器自己调度
- 按需拆分,避免加载无用代码
- 利用多路复用,一条连接搞定一切
记住这个核心原则:HTTP/2让你可以大胆地并行请求,但前提是你的代码要配合。
下次写 Fetch API 的时候,问自己三个问题:
- 这些请求可以并行吗?
- 我是否在浪费 HTTP/2 的多路复用?
- 用户真的需要等所有数据都回来吗?
更多网络协议与前端优化实战,欢迎在 云栈社区 交流讨论。