SPA首屏接口过多导致卡顿?一套前端请求调度方案彻底解决

一、真实问题场景

在实际项目中,你一定遇到过:

  • SPA 首屏加载,需要同时请求 5~10 个接口甚至更多
  • 用户频繁点击按钮,导致接口被重复触发
  • 某个接口不稳定,导致整个页面"卡死"
  • loading 状态混乱,用户体验极差

👉 这些问题本质都是:系统缺乏"请求调度能力"


二、问题本质拆解

我们把问题抽象一下:

1️⃣ 并发失控

浏览器会同时发起多个请求,容易:

  • 挤爆带宽
  • 阻塞关键请求
  • 导致接口限流

2️⃣ 重复请求

同一个接口被多次触发:

js 复制代码
getUserInfo()
getUserInfo()
getUserInfo()

👉 完全浪费资源


3️⃣ 无缓存机制

每次进入页面都重新请求:

👉 实际很多数据是可以复用的


4️⃣ 接口雪崩

某个接口挂了:

👉 页面一直重试 → 更加雪崩


三、解决思路(核心设计)

👉 一个统一模型:

text 复制代码
请求 → 调度器 → 控制执行

我们需要实现:

  • 队列(控制并发)
  • 去重(避免重复)
  • 缓存(减少请求)
  • 熔断(保护系统)
  • loading(统一管理)

四、队列:控制并发(核心)

为什么需要队列?

假设你同时请求10个接口:

text 复制代码
❌ 同时执行 → 网络阻塞
✅ 控制为3个 → 更稳定

实现代码(升级版)

js 复制代码
class RequestQueue {
  constructor(max = 5) {
    this.max = max;
    this.running = 0;
    this.queue = [];
  }

  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.run();
    });
  }

  run() {
    while (this.running < this.max && this.queue.length) {
      const { task, resolve, reject } = this.queue.shift();
      this.running++;

      Promise.resolve(task())
        .then(resolve)
        .catch(reject)
        .finally(() => {
          this.running--;
          this.run();
        });
    }
  }
}

使用示例

js 复制代码
const queue = new RequestQueue(3);

for (let i = 0; i < 10; i++) {
  queue.add(() => fetch("/api")).then(console.log);
}

五、请求去重:避免重复调用

场景

用户快速点击按钮:

👉 会触发多次相同请求


解决方案

js 复制代码
const pendingMap = new Map();

function requestWithDedupe(key, fn) {
  if (pendingMap.has(key)) {
    return pendingMap.get(key);
  }

  const promise = fn().finally(() => {
    pendingMap.delete(key);
  });

  pendingMap.set(key, promise);
  return promise;
}

核心思想

👉 相同 key 的请求只执行一次,其它复用 Promise


六、缓存机制:减少无效请求

场景

  • 用户信息
  • 配置数据
  • 字典数据

实现

js 复制代码
const cache = new Map();

function requestWithCache(key, fn, ttl = 5000) {
  const now = Date.now();

  if (cache.has(key)) {
    const { data, time } = cache.get(key);
    if (now - time < ttl) {
      return Promise.resolve(data);
    }
  }

  return fn().then((res) => {
    cache.set(key, { data: res, time: now });
    return res;
  });
}

优化点

  • 可加 localStorage
  • 可做持久缓存

七、熔断机制:防止系统雪崩(重点)

场景

接口连续失败:

text 复制代码
请求 → 失败
请求 → 失败
请求 → 失败

👉 如果继续请求,只会更糟


状态机

text 复制代码
CLOSED → 正常
OPEN → 熔断
HALF → 半开(试探)

实现

js 复制代码
class CircuitBreaker {
  constructor(limit = 3, timeout = 5000) {
    this.failCount = 0;
    this.limit = limit;
    this.state = "CLOSED";
    this.nextTry = 0;
    this.timeout = timeout;
  }

  async exec(fn) {
    if (this.state === "OPEN") {
      if (Date.now() < this.nextTry) {
        return Promise.reject("熔断中");
      }
      this.state = "HALF";
    }

    try {
      const res = await fn();
      this.success();
      return res;
    } catch (e) {
      this.fail();
      throw e;
    }
  }

  success() {
    this.failCount = 0;
    this.state = "CLOSED";
  }

  fail() {
    this.failCount++;
    if (this.failCount >= this.limit) {
      this.state = "OPEN";
      this.nextTry = Date.now() + this.timeout;
    }
  }
}

八、Loading 管理

问题

多个请求同时存在:

👉 loading 何时关闭?


解决方案

js 复制代码
let loadingCount = 0;

function showLoading() {
  if (loadingCount === 0) {
    console.log("loading start");
  }
  loadingCount++;
}

function hideLoading() {
  loadingCount--;
  if (loadingCount === 0) {
    console.log("loading end");
  }
}

九、最终整合:请求调度器

js 复制代码
class RequestScheduler {
  constructor() {
    this.queue = new RequestQueue(3);
    this.breaker = new CircuitBreaker();
    this.pending = new Map();
  }

  request(key, fn) {
    if (this.pending.has(key)) {
      return this.pending.get(key);
    }

    const task = async () => {
      showLoading();
      try {
        return await this.breaker.exec(fn);
      } finally {
        hideLoading();
      }
    };

    const p = this.queue.add(task).finally(() => {
      this.pending.delete(key);
    });

    this.pending.set(key, p);
    return p;
  }
}

相关推荐
大家的林语冰1 小时前
TypeScript 6 官宣,JS “最后之舞“,版本升级踩雷指南
前端·javascript·typescript
英俊潇洒美少年1 小时前
react useDeferredvalue和useTransition的讲解
前端·react.js·前端框架
爱学习的程序媛1 小时前
【WebRTC】呼叫中心前端技术选型:SIP.js vs JsSIP vs Verto
前端·javascript·typescript·音视频·webrtc·实时音视频·web
Amumu121381 小时前
Js: ES新特性(一)
开发语言·前端·javascript
scofield_gyb1 小时前
Redis 6.2.7安装配置
前端·数据库·redis
木斯佳1 小时前
前端八股文面经大全: 蓝色光标前端一面OC(2026-03-23)·面经深度解析
前端·面试·vue·校招·js·面经
2301_792580001 小时前
Pyrocko + PSGRN/PSCMP小问题
前端
Highcharts.js2 小时前
Highcharts for Python|用 Pythonic 的方式构建AI数据可视化图表
前端·人工智能·python·信息可视化·数据科学·highcharts·ai可视化
子豪-中国机器人2 小时前
python AI自动化
java·前端·python