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

相关推荐
无人生还16 分钟前
从 Vue3 到 React · 快速上手系列第 11 篇:状态管理
前端·vue.js·react.js
GuWenyue17 分钟前
90%前端写React TS都踩坑!一套父子组件+Hooks完整实战,彻底搞懂类型约束
前端·react.js
Hilaku27 分钟前
当 AI 一天写完一周的代码,我们还剩什么优势?
前端·javascript·程序员
尘世中一位迷途小书童31 分钟前
无人机航线里的云台角度怎么调?Cesium 做了个视锥 + 鹰眼联动
前端·javascript·数据可视化
思码梁田35 分钟前
CSS display 属性:从元素类型转换到隐藏元素的实用指南
前端·css·display·inline·block·none·inline-block
Csvn36 分钟前
⚡ Vite 依赖预构建(optimizeDeps)的 3 个经典坑:改了源码不生效、新增依赖 404、缓存怎么清都不行
前端
A24207349301 小时前
Vue3 + TypeScript:后端数据在表格内渲染后进行增删改的完整实现步骤
前端·javascript·typescript
他们叫我 悦儿遥遥雨1 小时前
.NET 8 Web开发入门(六):Blazor 全栈开发——告别 JavaScript 焦虑
前端·javascript·.net
用户2181697049301 小时前
Flutter(九)StatelessWidget StatefullWidget
前端
paopaokaka_luck1 小时前
基于springboot3+vue3的乡村医生诊疗管理系统(AI助手、协同过滤算法、webSocket实时聊天、Echarts图形化分析)
前端·网络·人工智能·spring boot·websocket·网络协议·echarts