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

相关推荐
码海扬帆:前端探索之旅2 小时前
深度定制 uni-combox:新增功能详解与实战指南
前端·vue.js·uni-app
谷雨不太卷2 小时前
进程的状态码
java·前端·算法
打小就很皮...2 小时前
基于 Python + LangChain + RAG 的知识检索系统实战
前端·langchain·embedding·rag
BJ-Giser2 小时前
Cesium 烟雾粒子特效
前端·可视化·cesium
空中海2 小时前
02 ArkTS 语言与工程规范
java·前端·spring
YJlio2 小时前
7.4.5 Windows 11 企业网络连接与网络重置实战:远程访问、本地策略与故障恢复
前端·chrome·windows·python·edge·机器人·django
Slow菜鸟3 小时前
Codex CLI 教程(五)| Skills 安装指南:面向 Java 全栈工程师打造个人 ECC(V1版)
大数据·前端·人工智能
Lee川3 小时前
打字机是怎么炼成的:Chat 流式输出深度解析
前端·后端·面试
前端若水3 小时前
过渡(transition)高级:贝塞尔曲线、硬件加速
前端·css·css3
Lee川3 小时前
Token 无感刷新与 Logout:前端安全会话管理实战
前端·后端·react.js