一、核心思路(一句话)
核心不是单纯"让请求更快",而是从"少请求、控并发、做聚合、命中缓存、减小传输、缩短网络距离"六个方向降低请求总成本。
二、主要矛盾与次要矛盾
主要矛盾
大量请求真正容易造成的问题是:
- 请求数量太多 → 网络连接、调度、服务端压力增加。
- 并发过高 → 浏览器、网络、服务器资源被大量占用。
- 请求链路过长 → 一个请求依赖另一个请求,整体耗时被拉长。
- 重复请求 → 相同数据反复从服务器获取。
次要矛盾
在上述问题解决后,再进一步优化:
- 响应体过大 → 压缩、减少字段。
- 网络距离远 → CDN。
- HTTP 协议效率低 → HTTP/2、HTTP/3。
- 静态资源重复传输 → 浏览器缓存。
- 不重要请求占用资源 → 优先级、延迟加载。
面试时不要一上来就说"开启压缩、上 CDN"。
先判断瓶颈到底是请求数量、并发、链路、缓存还是传输体积。
三、解决方案流程图
text
大量 HTTP / Ajax 请求
│
▼
┌── 请求是不是太多? ──┐
│ │
是 否
│ │
▼ ▼
请求合并 / BFF聚合 是否存在重复请求?
减少前端请求数量 │
是
│
▼
缓存 / 请求去重
│
▼
是否需要控制并发?
│
▼
并发队列 / 优先级
│
▼
数据传输是否过大?
│
▼
Brotli / Gzip / 数据裁剪
│
▼
网络距离是否过远?
│
▼
CDN
│
▼
HTTP/2 / HTTP/3 多路复用
实际项目中通常是组合使用,而不是只能选择一种方案。
四、结构化分析
1. 控制并发:请求队列
核心思路
不是一次性把1000个请求全部发出去,而是限制同时执行的请求数量,例如最多同时执行5个。
text
1000个任务
│
▼
┌───────────────┐
│ 请求任务队列 │
└───────────────┘
│
├──→ 请求1
├──→ 请求2
├──→ 请求3
├──→ 请求4
└──→ 请求5
↓
完成一个
↓
再取一个
使用场景
例如:
- 批量上传1000张图片
- 批量请求1000个商品详情
- 批量调用第三方接口
- 批量处理文件
如果一次性 Promise.all():
js
await Promise.all(
urls.map(url => fetch(url))
);
可能瞬间创建大量请求任务。
更合理的是限制并发:
js
async function runWithConcurrency(tasks, limit) {
const results = new Array(tasks.length);
let nextIndex = 0;
// 每个 worker 不断从任务队列中领取任务
async function worker() {
while (true) {
const index = nextIndex++;
// 没有任务了就结束
if (index >= tasks.length) return;
try {
// 执行当前任务
results[index] = await tasks[index]();
} catch (error) {
// 保存错误,避免一个任务失败导致整个队列停止
results[index] = { error };
}
}
}
// 最多同时启动 limit 个 worker
const workers = Array.from(
{ length: Math.min(limit, tasks.length) },
() => worker()
);
await Promise.all(workers);
return results;
}
// 示例
const tasks = Array.from({ length: 100 }, (_, index) => {
return () =>
fetch(`/api/item/${index}`).then(response => response.json());
});
runWithConcurrency(tasks, 5);
更好的工程方案
实际项目可以直接使用成熟的并发限制库,例如 p-limit,避免自己维护队列。
js
import pLimit from 'p-limit';
const limit = pLimit(5);
const tasks = urls.map(url =>
limit(() => fetch(url))
);
const results = await Promise.all(tasks);
注意
并发限制不是越小越好。
text
并发太大 → 资源竞争、网络拥塞、服务器压力
并发太小 → 网络资源没有充分利用、整体耗时增加
所以实际应该根据:
- 浏览器环境
- 网络情况
- 接口响应时间
- 服务端承载能力
- 请求类型
进行压测和调整。
五、请求优先级
如果请求很多,不仅要控制数量,还可以考虑:
text
高优先级
↓
首屏核心数据
用户当前操作
关键接口
↓
低优先级
↓
埋点
预加载
非首屏数据
例如:
js
const highPriorityTasks = [
loadUserInfo(),
loadProductInfo()
];
const lowPriorityTasks = [
loadRecommendations(),
loadAnalytics()
];
实际项目中可以进一步设计:
text
高优先级队列
↓
中优先级队列
↓
低优先级队列
↓
并发调度器
核心是让有限的网络资源优先服务真正影响用户体验的请求。
六、减少请求数量:BFF / 接口聚合
这是非常重要、而且比"多域名"更核心的方案之一。
假设页面需要:
text
用户信息
↓
收藏文章
↓
文章详情
↓
评论数量
↓
点赞数量
如果前端直接访问微服务:
text
Browser
│
├──→ 用户服务
├──→ 收藏服务
├──→ 文章服务
├──→ 评论服务
└──→ 点赞服务
不仅请求多,而且可能存在请求依赖:
text
获取用户
↓
获取收藏文章
↓
获取文章详情
↓
获取评论
这会让整个页面的网络瀑布变长。
使用 BFF
可以增加一层 Backend For Frontend:
text
Browser
│
│ 1次请求
▼
BFF层
┌───────┼───────┐
↓ ↓ ↓
用户服务 文章服务 评论服务
│ │ │
└───────┼───────┘
↓
聚合结果
│
▼
Browser
前端只需要:
js
const response = await fetch('/api/home');
const data = await response.json();
BFF 内部再并行请求:
js
const [user, articles, comments] = await Promise.all([
getUser(),
getArticles(),
getComments()
]);
return {
user,
articles,
comments
};
BFF 的价值
不是简单"把请求转发一下",而是把面向前端页面的数据需求进行聚合。
它可以:
- 聚合多个微服务
- 裁剪无用字段
- 统一接口格式
- 并行调用后端服务
- 做缓存
- 做鉴权
- 做降级和容错
七、缓存:减少重复请求
缓存的核心:
text
第一次请求
↓
服务器
↓
得到结果
↓
缓存
第二次相同请求
↓
缓存命中
↓
直接返回
但这里有一个需要纠正的地方:
不能简单地说"幂等接口就可以缓存"。
这是两个不同的概念。
幂等性
幂等性解决的是:
同一个请求执行一次和执行多次,对服务器最终状态的影响是否相同。
例如:
http
PUT /user/1
反复设置:
json
{
"name": "张三"
}
最终状态仍然是:
text
name = 张三
这具有幂等性。
可缓存性
可缓存性关注:
这个请求的响应是否适合被复用,以及复用多久、什么条件下失效。
所以:
text
幂等 ≠ 可缓存
可缓存 ≠ 幂等
例如:
text
GET /user/1
通常适合缓存,但具体是否缓存以及缓存多久,还要看数据实时性和 HTTP 缓存策略。
八、缓存方案
1. HTTP 强缓存 / 协商缓存
对于浏览器资源,优先使用标准 HTTP 缓存机制。
典型流程:
text
浏览器请求
↓
本地缓存?
│
├── 新鲜 → 直接使用
│
└── 过期
↓
向服务器验证
↓
未修改 → 304
│
└── 使用本地缓存
常见机制:
text
Cache-Control
ETag
Last-Modified
其中 ETag 可以理解为服务器给资源生成的版本标识。
九、请求级缓存 / 哈希缓存
对于应用层请求,也可以建立:
text
请求
↓
生成 Cache Key
↓
查询缓存
├── 命中 → 返回缓存
└── 未命中
↓
发请求
↓
保存结果
例如:
js
function createCacheKey(url, options = {}) {
return JSON.stringify({
url,
method: options.method || 'GET',
headers: options.headers || {},
body: options.body || null
});
}
const cache = new Map();
async function requestWithCache(url, options = {}) {
const key = createCacheKey(url, options);
// 相同请求直接复用结果
if (cache.has(key)) {
return cache.get(key);
}
const promise = fetch(url, options).then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
// 缓存 Promise,可以避免"并发重复请求"
// 例如两个组件同时请求同一个接口,只发送一次真实网络请求
cache.set(key, promise);
try {
return await promise;
} catch (error) {
// 请求失败不能长期留在缓存里,否则后续请求都会复用失败结果
cache.delete(key);
throw error;
}
}
这里实际上还有一个很实用的优化:
请求去重
text
组件A ─┐
├──→ /api/user/1 ──→ 真实请求一次
组件B ─┘
而不是:
text
组件A ──→ /api/user/1
组件B ──→ /api/user/1
这叫请求去重 / 请求合并,在大型前端应用里非常实用。
十、减少传输体积:压缩
第一层:资源本身优化
text
图片
→ WebP / AVIF
→ 合理尺寸
→ 图片压缩
→ 懒加载
JavaScript
→ Tree Shaking
→ Minification
→ Code Splitting
第二层:HTTP 传输压缩
服务器可以对文本资源进行:
text
Brotli
Gzip
例如:
http
Content-Encoding: br
浏览器解压后再使用。
Brotli / Gzip 主要适合 HTML、CSS、JavaScript、JSON 等文本数据;已经压缩过的 JPEG、PNG、WebP、AVIF 通常再次压缩收益很小。
十一、CDN
CDN解决的核心不是"让接口代码执行更快",而是:
让资源尽可能从距离用户更近的节点返回。
text
源站
│
┌──────┼──────┐
↓ ↓ ↓
CDN节点 CDN节点 CDN节点
↑ ↑ ↑
│ │ │
用户A 用户B 用户C
特别适合:
- JavaScript
- CSS
- 图片
- 字体
- 视频
- 其他静态资源
对于动态 API,也可以在合适的架构下使用 CDN / 边缘缓存,但需要考虑数据实时性、鉴权和缓存一致性。
十二、多域名并发:属于历史优化手段
过去 HTTP/1.1 下,浏览器通常会对同一源的并发连接数量进行限制,所以早期会采用:
text
static1.example.com
static2.example.com
static3.example.com
把资源分散到多个域名。但是:
现代 HTTP/2 / HTTP/3 已经通过多路复用显著降低了这种需求,因此不应该把"域名分片"作为现代前端的首选优化方案。
甚至域名过多还可能增加:
- DNS 查询
- TCP/TLS 建连
- 连接管理成本
所以现在更应该考虑:
text
HTTP/1.1
↓
必要时域名分片
HTTP/2
↓
多路复用
↓
通常不需要域名分片
HTTP/3
↓
QUIC + 多路复用
↓
进一步优化传输效率
十三、HTTP/2 为什么能减少这个问题?
HTTP/1.1:
text
多个请求
↓
多个连接 / 请求调度
↓
连接并发受限制
HTTP/2:
text
一个 TCP 连接
│
├── Stream 1 → 请求A
├── Stream 2 → 请求B
├── Stream 3 → 请求C
├── Stream 4 → 请求D
└── Stream 5 → 请求E
通过多路复用,多个请求可以在同一连接上交错传输。
HTTP/3 则基于 QUIC,将传输层从 TCP 转移到 UDP 之上的 QUIC,并进一步解决 TCP 层队头阻塞带来的部分问题。
十四、一个完整的优化架构
text
Browser
│
┌─────────┴─────────┐
│ │
静态资源 API请求
│ │
▼ ▼
CDN BFF
│
┌────────────┼────────────┐
↓ ↓ ↓
用户服务 文章服务 评论服务
│ │ │
└────────────┼────────────┘
↓
聚合结果
Browser侧:
─────────────────────────────────────
请求去重
请求缓存
并发控制
优先级调度
懒加载
数据裁剪
─────────────────────────────────────
网络层:
─────────────────────────────────────
HTTP/2 / HTTP/3
Brotli / Gzip
CDN
HTTP缓存
─────────────────────────────────────
十五、边界场景
场景1:1000个独立 API 请求
重点:
text
请求队列
+
并发限制
+
请求去重
+
缓存
场景2:一个页面需要调用10个微服务
重点不是简单限制并发,而是:
text
BFF
↓
服务端并行调用
↓
聚合数据
↓
一次返回给前端
减少前端网络往返和请求编排复杂度。
场景3:大量静态资源
重点:
text
CDN
+
浏览器缓存
+
压缩
+
图片格式优化
+
Code Splitting
+
懒加载
场景4:接口数据实时变化
不要简单设置一个很长的本地缓存时间。应该根据业务选择:
text
强缓存
协商缓存
短时缓存
SWR
主动失效
重新验证
场景5:同一个接口被多个组件同时调用
优先考虑:
text
请求去重
例如:
text
组件A ─┐
组件B ─┼──→ 同一个 Promise ──→ 一次 HTTP 请求
组件C ─┘
这比单纯的"请求完成后缓存"更进一步,因为它解决的是请求正在进行中的重复请求。
十六、面试答题的结构化逻辑
遇到"1000个请求怎么优化",可以按照这个顺序思考:
text
第一问:能不能不请求?
↓
缓存 / 请求去重 / 数据预取策略
第二问:能不能少请求?
↓
BFF / 接口聚合 / 批量接口
第三问:必须请求的话,能不能少并发?
↓
并发队列 / 优先级
第四问:请求必须发送,能不能少传数据?
↓
字段裁剪 / 压缩 / 图片优化
第五问:网络距离能不能缩短?
↓
CDN / 边缘节点
第六问:传输协议能不能更高效?
↓
HTTP/2 / HTTP/3
这套思路比单纯罗列:
p-limit + 缓存 + CDN + Gzip + 多域名
更像真正的工程方案。
十七、满分答案
核心思路:面对大量 HTTP 请求,我会按照"少请求 → 控并发 → 做聚合 → 用缓存 → 减体积 → 优化网络"的顺序优化,而不是单纯提高并发。
解决流程
text
大量请求
↓
① 能不能不请求?
→ 缓存、请求去重
↓
② 能不能少请求?
→ BFF、接口聚合、批量接口
↓
③ 必须请求,能不能控制并发?
→ 请求队列、优先级
↓
④ 数据能不能更小?
→ 字段裁剪、Brotli/Gzip、图片优化
↓
⑤ 网络距离能不能缩短?
→ CDN
↓
⑥ 协议能不能更高效?
→ HTTP/2、HTTP/3
1. 控制并发
如果一次要发1000个请求,我不会直接 Promise.all 全部发出去,而是建立请求队列,比如同时最多执行5个任务,完成一个再补充一个;如果还涉及首屏和非首屏请求,可以增加优先级调度。
2. 减少请求数量
如果前端为了一个页面需要调用多个微服务,我会考虑 BFF(Backend For Frontend),由中间层统一调用多个后端服务并聚合结果,前端只需要请求一次。服务之间还可以并行调用,减少前端的网络往返和请求编排。
text
Browser
↓
BFF
┌─┼─┐
↓ ↓ ↓
用户 文章 评论服务
└─┼─┘
↓
聚合结果
↓
Browser
3. 缓存和请求去重
对于适合缓存的数据,可以使用 HTTP 缓存或者应用层缓存。
另外,如果多个组件同时请求同一个接口,可以直接复用正在进行中的 Promise,让多个调用共享一次真实 HTTP 请求。
需要注意:幂等性不等于可缓存性,是否缓存应该根据接口语义、数据实时性和缓存策略决定。
4. 减少传输体积
JSON、JavaScript、CSS、HTML 等文本数据可以使用 Brotli 或 Gzip;图片则应该通过合理尺寸、图片压缩以及 WebP、AVIF 等格式减少体积。对于接口还应该只返回真正需要的字段。
5. CDN
静态资源可以放到 CDN,让用户从距离更近的节点获取资源,降低网络延迟。
6. HTTP/2 和 HTTP/3
现代项目应该优先使用 HTTP/2 或 HTTP/3。HTTP/2 可以通过多路复用让多个请求共享连接,因此以前常见的"域名分片"已经不是现代项目的首选方案。
所以真正的优化思路不是简单地"让1000个请求更快",而是先想办法让它变成更少的请求;必须请求时再控制并发、缓存和优先级,最后再从传输体积、CDN和协议层继续优化。
这道题最容易被面试官追问的三个点:
- 为什么不用
Promise.all一次发完? → 并发过高,资源竞争和服务端压力增加。 - BFF到底解决什么? → 聚合多个微服务,减少前端请求数量和网络往返,同时把服务编排放到服务端。
- 幂等接口是不是都能缓存? → 不是。幂等性解决"重复执行的状态影响",缓存解决"响应能否复用",两者是不同概念。