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;
  }
}

相关推荐
发现一只大呆瓜7 小时前
深度解密 Rollup 插件开发:核心钩子函数全生命周期图鉴
前端·vite
java_nn7 小时前
一文了解前端技术
前端
发现一只大呆瓜7 小时前
深度解析 Rollup 配置与 Vite 生产构建流程
前端·vite
小码哥_常8 小时前
安卓黑科技:让手机成为你的“跌倒保镖”
前端
小李子呢02119 小时前
前端八股Vue---Vue2和Vue3的区别,set up的用法
前端·javascript·vue.js
m0_647057969 小时前
Harness Engineering 实践指南
前端
JJay.9 小时前
Android BLE 稳定连接的关键,不是扫描,而是 GATT 操作队列
android·服务器·前端
星空椰9 小时前
JavaScript 进阶基础:函数、作用域与常用技巧总结
开发语言·前端·javascript
奔跑的呱呱牛9 小时前
@giszhc/vue-page-motion:Vue3 路由动画怎么做才“丝滑”?(附在线示例)
前端·javascript·vue.js
ThridTianFuStreet小貂蝉10 小时前
面试题4:讲一讲HTML5、CSS3新特性
前端·css3·html5