如何限制并发请求的数量避免浏览器卡死?

一淑涵 阅读 37

我有个页面要批量上传几十个文件,直接用 Promise.all 发起请求,结果浏览器直接卡住了,甚至报错“Too many connections”。

试过自己写了个队列控制,但逻辑有点乱,比如下面这样:

async function uploadWithLimit(files, limit = 3) {
  const results = [];
  for (let i = 0; i < files.length; i += limit) {
    const chunk = files.slice(i, i + limit);
    const promises = chunk.map(file => fetch('/upload', { method: 'POST', body: file }));
    results.push(...await Promise.all(promises));
  }
  return results;
}

但这样还是会在每批内并发太多,而且不能动态调度空闲的“槽位”。有没有更优雅的并发控制方案?

我来解答 赞 10 收藏
二维码
手机扫码查看
2 条解答
Mr.瑞红
Mr.瑞红 Lv1
可以试试用一个任务队列加上计数器来控制并发数量。这个方案比较灵活,能动态调整正在执行的任务数。

先准备一个函数来管理并发:

async function limitConcurrency(tasks, limit) {
let running = 0;
const results = [];
const execute = async (task, index) => {
running++;
try {
const result = await task();
results[index] = result;
} finally {
running--;
if (queue.length > 0 && running < limit) {
queue.shift()();
}
}
};

const queue = tasks.map((task, index) => () => execute(task, index));

for (let i = 0; i < Math.min(limit, queue.length); i++) {
queue[i]();
}

while (running > 0) {
await new Promise(resolve => setTimeout(resolve));
}

return results;
}


然后这样调用:

const uploadTasks = files.map(file => () => fetch('/upload', { method: 'POST', body: file }));
const results = await limitConcurrency(uploadTasks, 3);


这个方法会限制同时执行的请求为指定的数量,等某个请求完成后才会启动下一个等待中的请求。写这种代码真是让人头大,不过确实比直接用 Promise.all 稳定多了,至少不会把浏览器搞崩。
点赞
2026-03-31 08:17
小金宇
小金宇 Lv1
你这段代码的问题在于它是"分批发送"而不是"滑动窗口",每批必须全部结束才能开始下一批,效率不高。建议改成用 Promise.race 配合 Set 来实现一个真正的并发池。

核心思路是:始终维护一个正在执行的 Promise 集合,一旦有请求完成,立刻补上下一个,这样永远保持最大并发数,不会出现空窗期。

async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();

for (const [index, task] of tasks.entries()) {
// 并发数达到上限时,等待任意一个完成
if (executing.size >= limit) {
await Promise.race(executing);
}

const p = Promise.resolve()
.then(() => task())
.then(result => {
results[index] = result;
})
.finally(() => executing.delete(p));

executing.add(p);
}

// 等待剩余任务全部完成
await Promise.all(executing);
return results;
}


然后你的上传函数就变得很干净了:

async function uploadWithLimit(files, limit = 3) {
const tasks = files.map(file => () =>
fetch('/upload', { method: 'POST', body: file })
);
return limitConcurrency(tasks, limit);
}


这里有几个关键点需要说明一下。

Set 存储正在执行的 Promise,配合 finally 自动删除已完成的任务,这样 Promise.race 每次都能正确感知到"有空位了"。

任务要包装成函数 () => fetch(...) 而不是直接传 Promise,否则循环的时候所有请求就已经发出去了,根本控制不住。

结果用索引赋值 results[index] = result,这样即使任务完成顺序乱掉,最终结果顺序还是跟输入保持一致。

这种写法比你自己写的 for 循环分批要优雅很多,而且能最大化利用并发槽位,不会出现"等最慢的那个"的情况。
点赞 1
2026-03-03 01:05