前端处理大量并发请求的实用技巧
作为一个在前端摸爬滚打几年的开发者,我深知处理大量并发请求时的崩溃瞬间:用户点个按钮,页面卡成PPT;同时发10个请求,浏览器直接罢工;请求失败后不知道怎么重试......这篇就聊聊我在实际项目中踩过的坑,以及总结的能直接用的解决方案。
一、先搞懂问题:为什么并发请求会出问题?
浏览器不是永动机,发请求时它会遇到这些限制:
1. 浏览器的"自私"限制
- 同域名最多同时发6-8个请求(Chrome默认6个):超过的部分会被排队,等你前面的请求完成才能发。
- TCP队首阻塞:一个请求的数据包丢了,后面的得等它重传,导致后续请求延迟。
- 内存/CPU不够用:同时发太多请求,浏览器内存可能爆掉,页面卡顿甚至崩溃。
2. 网络的"不靠谱"
- 高延迟时,请求堆在服务器门口,用户看到"加载中..."半天没反应。
- 弱网环境下,请求容易超时或失败,用户反复点击,雪上加霜。
3. 业务的"麻烦事"
- 用户点了"批量删除10条数据",前端得同时发10个删除请求,但服务器可能处理不过来。
- 实时仪表盘要同时拉取5个指标,有的快有的慢,页面数据忽闪忽闪的。
二、从简单到进阶:我能直接用的解决方案
1. 基础版:限制并发数,别让浏览器"堵车"
问题场景 :用户点了"导出100条数据",前端循环发100个请求,结果浏览器卡死。
解决思路:别一次性发所有请求,控制同时发的数量(比如每次发5个),等前面的完成了再发下一批。
代码实现(简单版并发控制):
typescript
// 发请求的函数数组(比如100个删除请求)
const requestFns = Array.from({ length: 100 }, (_, i) =>
() => fetch(`/api/delete/${i}`, { method: 'DELETE' })
);
// 控制并发数的函数(关键!)
async function runWithConcurrency(tasks, maxConcurrent = 5) {
const results = [];
const running = new Set();
for (const task of tasks) {
// 如果同时跑的任务没超过上限,直接发
if (running.size < maxConcurrent) {
const p = task().finally(() => running.delete(p));
running.add(p);
results.push(p);
} else {
// 等前面的任务有一个完成,再发新的
await Promise.race(running);
// 重新检查队列(这里简化了,实际可能需要递归)
}
}
// 等所有任务完成
await Promise.all(running);
return results;
}
// 使用:同时最多发5个请求
runWithConcurrency(requestFns, 5).then(() => {
console.log('全部导出完成!');
});
注意 :实际项目中,这个函数需要加重试 (比如某个请求失败了,自动再发一次)、超时 (比如等5秒没响应就取消)、进度反馈(告诉用户"已完成30%")。
2. 进阶版:别重复发请求,做个"请求去重"
问题场景 :用户快速点击"刷新数据"按钮,前端发了10次同一个请求,服务器压力大,页面数据乱闪。
解决思路:发请求前先检查"这个请求我发过了吗?",如果正在发或已经发过,就等结果,别重复发。
代码实现(带缓存的请求去重):
typescript
// 用Map存正在进行的请求(key是请求的URL+参数)
const requestCache = new Map<string, Promise<any>>();
// 发请求的函数(带去重)
async function fetchWithCache(url: string, params?: any) {
// 生成唯一的请求key(把参数拼到URL里)
const key = `${url}?${new URLSearchParams(params).toString()}`;
// 如果这个请求已经在发了,直接等结果
if (requestCache.has(key)) {
return requestCache.get(key);
}
// 否则,发请求并缓存Promise
const promise = fetch(url, { method: 'POST', body: JSON.stringify(params) })
.then(res => res.json())
.finally(() => {
// 请求完成后,从缓存里删掉(避免内存泄漏)
requestCache.delete(key);
});
requestCache.set(key, promise);
return promise;
}
// 使用:不管点多少次,只会发一次请求
fetchWithCache('/api/data', { page: 1 }).then(data => {
console.log('数据加载完成', data);
});
升级 :如果需要"过期时间"(比如5分钟内重复请求直接用缓存),可以给requestCache
加个时间戳,检查是否过期。
3. 实战版:批量操作的"进可攻退可守"
问题场景 :用户点了"批量删除10条数据",但其中3条删除失败,页面没反应,用户不知道咋回事。
解决思路:
- 发请求时记录每个任务的状态(等待/成功/失败)。
- 失败的任务可以重试,或者让用户手动重试。
- 实时更新进度,让用户知道"到哪一步了"。
代码实现(批量操作管理器):
typescript
class BatchManager {
// 存所有任务的状态(id、状态、进度)
tasks: Array<{
id: string;
status: 'waiting' | 'running' | 'success' | 'fail';
progress?: number;
}> = [];
// 最大同时并发数
maxConcurrent = 3;
// 添加任务(比如删除10条数据)
addTasks(taskIds: string[]) {
// 初始化任务状态
this.tasks = taskIds.map(id => ({
id,
status: 'waiting'
}));
// 开始处理任务
this.processTasks();
}
// 处理任务(核心逻辑)
private async processTasks() {
// 找出可以跑的任务(状态是waiting,且当前跑的任务没超过上限)
const waitingTasks = this.tasks.filter(t => t.status === 'waiting');
const runningCount = this.tasks.filter(t => t.status === 'running').length;
// 如果没任务要跑,或者跑满了,就等
if (waitingTasks.length === 0 || runningCount >= this.maxConcurrent) {
setTimeout(() => this.processTasks(), 500); // 等500ms再检查
return;
}
// 跑最多maxConcurrent个任务
const toRun = waitingTasks.slice(0, this.maxConcurrent - runningCount);
toRun.forEach(task => {
task.status = 'running';
this.updateUI(); // 更新页面显示
// 发请求(模拟删除接口)
fetch(`/api/delete/${task.id}`, { method: 'DELETE' })
.then(() => {
task.status = 'success';
})
.catch(() => {
task.status = 'fail';
})
.finally(() => {
this.updateUI(); // 请求完成后更新页面
this.processTasks(); // 继续处理下一个任务
});
});
}
// 更新页面(比如显示进度条、颜色标记)
private updateUI() {
// 这里可以调用页面的渲染函数,比如:
// document.getElementById('task-list').innerHTML = this.tasks.map(t => `
// <div class="task ${t.status}">${t.id}</div>
// `).join('');
}
}
// 使用:添加10个删除任务
const manager = new BatchManager();
manager.addTasks(['task1', 'task2', ..., 'task10']);
扩展:
- 失败的任务可以加"重试"按钮,点击后重新加入任务队列。
- 进度条可以用
task.progress
来更新(比如上传文件时,显示已传百分比)。
4. 终极版:用WebSocket"实时唠嗑"
问题场景 :实时仪表盘要每3秒拉一次5个指标,每次发5个请求,页面数据忽闪,还容易超时。
解决思路:用WebSocket和服务器"保持聊天",服务器有新数据时主动推过来,不用前端反复发请求。
代码实现(WebSocket连接管理):
typescript
class RealTimeData {
private socket: WebSocket | null = null;
// 存每个指标的最新数据
data: Record<string, any> = {};
// 连接服务器
connect(url: string) {
this.socket = new WebSocket(url);
// 连接成功后,告诉服务器我要哪些指标
this.socket.onopen = () => {
this.socket.send(JSON.stringify({
type: 'subscribe',
metrics: ['cpu', 'memory', 'network', 'disk', 'users']
}));
};
// 收到服务器推送的数据
this.socket.onmessage = (event) => {
const { metric, value } = JSON.parse(event.data);
this.data[metric] = value;
this.updateUI(); // 更新页面
};
// 连接断了,自动重连
this.socket.onclose = () => {
console.log('连接断了,5秒后重连...');
setTimeout(() => this.connect(url), 5000);
};
}
// 页面更新(比如渲染图表)
private updateUI() {
// 这里可以调用ECharts等库渲染数据
console.log('数据更新', this.data);
}
}
// 使用:连接到服务器,实时获取数据
const realTime = new RealTimeData();
realTime.connect('wss://your-server.com/ws');
注意:WebSocket适合"实时性高、数据变化频繁"的场景(比如聊天、监控),如果是"一次性拉取大量数据",还是HTTP更合适。
三、避坑指南:这些坑我踩过!
- 别滥用并发控制:并发数不是越小越好,太小会导致请求变慢(比如同时发2个,不如发5个快)。根据浏览器限制(6-8)和服务端承受能力调。
- 请求去重要加过期时间:不然用户刷新页面后,旧的缓存请求可能还在,导致数据过时。
- 失败重试别无脑重试:比如用户删除数据,第一次失败可能是权限问题,重试10次也没用,应该提示用户"删除失败,请检查权限"。
- 别让请求"堵死"页面:如果请求卡住了,页面应该给提示(比如"网络开小差了,点击重试"),而不是干等。
总结
处理大量并发请求,核心就3件事:
- 控制节奏:别让浏览器/服务器"堵车",用并发控制函数限制同时发的数量。
- 避免重复:用缓存记录已发的请求,别让用户点一下按钮发10次同样的请求。
- 兜底处理:失败了能重试,卡住了能提示,让用户知道"发生了啥"。
这些方案都是我在实际项目中验证过的,直接复制就能用。遇到具体问题(比如弱网环境、超大数据量),再针对性调整就行~