面试题:页面需要发起大量 Ajax / HTTP 请求时,如何优化请求性能?

一、核心思路(一句话)

核心不是单纯"让请求更快",而是从"少请求、控并发、做聚合、命中缓存、减小传输、缩短网络距离"六个方向降低请求总成本。


二、主要矛盾与次要矛盾

主要矛盾

大量请求真正容易造成的问题是:

  1. 请求数量太多 → 网络连接、调度、服务端压力增加。
  2. 并发过高 → 浏览器、网络、服务器资源被大量占用。
  3. 请求链路过长 → 一个请求依赖另一个请求,整体耗时被拉长。
  4. 重复请求 → 相同数据反复从服务器获取。

次要矛盾

在上述问题解决后,再进一步优化:

  • 响应体过大 → 压缩、减少字段。
  • 网络距离远 → 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和协议层继续优化。

这道题最容易被面试官追问的三个点:

  1. 为什么不用 Promise.all 一次发完? → 并发过高,资源竞争和服务端压力增加。
  2. BFF到底解决什么? → 聚合多个微服务,减少前端请求数量和网络往返,同时把服务编排放到服务端。
  3. 幂等接口是不是都能缓存? → 不是。幂等性解决"重复执行的状态影响",缓存解决"响应能否复用",两者是不同概念。
相关推荐
linux_cfan1 小时前
videojs v10 源代码系列解读:30 · `SerialRunner` 与 `ConcurrentRunner`
前端
lyz2468592 小时前
vue3页面因参数未声明而报错的常犯错误
前端·javascript·vue.js
摸爬滚打的小李2 小时前
流程图同步AI生成
前端
Hilaku2 小时前
我重写了整个项目,但没人感谢我!
前端·javascript·程序员
零域码客2 小时前
零基础Python爬虫入门:从HTTP协议到 requests + BeautifulSoup 实战
爬虫·python·http
zach3 小时前
Vue/React SPA 打包部署后,子路由刷新 401 未登录问题彻底解决
前端·nginx·next.js
旋生万物3 小时前
素数螺旋映射 $z_n=n^{1+i}$ 的角分布统计检验与零模型对比
大数据·前端·人工智能·算法·云原生·螺旋生成论·螺旋相位
亿元程序员3 小时前
游戏引擎都没用!纯AI又上线了一款蚂蚁搬家小游戏!
前端
福兮说3 小时前
JS 正则的六个坑:带 g 的 test() 一真一假、空匹配死循环、replace 里的 $
开发语言·前端·javascript·正则表达式