电商网站同时加载商品详情推荐列表和价格更新时浏览器对AJAX并发请求的数量限制及如何处理多个异步请求的完整指南
开篇先聊聊你遇到的这个坑
你有没有遇到过这种情况——打开一个电商页面,发现商品推荐列表和实时价格刷新同时触发,页面直接卡成PPT?或者更糟,价格显示错误、推荐内容丢失?别慌,这不是你代码写得烂,而是你撞上了浏览器对AJAX并发请求的那道”隐形天花板”。
今天咱就好好聊聊这件事,从浏览器底层限制到实战解决方案,一个一个给你掰扯清楚。
浏览器AJAX并发限制的真相
并发限制到底是多少?
这里有个很多人不知道的冷知识:浏览器对同一个域名(host)的并发连接数是有硬性限制的。不同浏览器的限制还不太一样:
| 浏览器 | 同一域名最大并发连接数 |
|---|---|
| Chrome | 6 |
| Firefox | 6 |
| Safari | 6 |
| Edge | 6 |
| IE8+ | 6 |
注意,是同一域名,不是整个浏览器。也就是说,你对 www.example.com 最多同时发6个AJAX请求,剩下的请求会排队等待。
为什么要有这个限制?
你可能会想,为啥不让我多开点连接呢?这其实是浏览器在替你”省事儿”:
- 服务器扛不住:想象一下,如果每个用户都能同时发100个请求,电商网站几百万用户同时访问,服务器直接爆炸
- 网络资源优化:TCP连接建立本身就有开销(三次握手),盲目堆连接反而拖慢整体速度
- 防止网络拥塞:同一个源的大量并发请求会占满带宽,影响其他正常流量的传输
所以这个限制本质上是浏览器和服务器之间的一种”默契”。
电商场景下的典型问题
场景还原
假设你现在在逛一个电商网站,页面同时需要做这几件事:
1. 获取商品详情数据(API: /api/product/detail?id=12345)
2. 获取商品推荐列表(API: /api/product/recommend?id=12345)
3. 获取实时价格(API: /api/product/price?id=12345)
4. 获取库存信息(API: /api/product/stock?id=12345)
5. 获取用户优惠券(API: /api/user/coupon?id=12345)
6. 获取商品评价(API: /api/product/reviews?id=12345)
7. 获取推荐商品图片预加载(图片资源)
8. 获取推荐商品缩略图
好家伙,一口气8个请求,浏览器只能同时处理6个,剩下2个排队。如果页面加载时还有更多的资源(CSS、JS、字体),这些排队时间会进一步被拉长。
实际影响
时间线演示:
T=0ms → 请求1-6同时发出
T=0ms → 请求7-8进入等待队列
T=300ms → 请求1响应回来,请求7入队
T=350ms → 请求2响应回来,请求8入队
T=400ms → 请求7响应回来
T=420ms → 请求8响应回来
表面上看,多了200毫秒,但实际上用户感知到的”页面加载完成”时间(DOMContentLoaded / load事件)会明显延迟。对于电商网站来说,每延迟100毫秒,转化率可能下降1%——这笔账算下来可不小。
解决方案一:请求优先级与队列管理
核心思路
不是所有请求都是”平等”的。商品详情和价格肯定是最高优先级的,推荐列表和图片预加载可以稍微等一下。
实现方案
下面我用一个真实的JavaScript实现来展示:
class RequestQueue {
constructor(maxConcurrency = 6) {
this.maxConcurrency = maxConcurrency;
this.running = 0;
this.queue = [];
}
/**
* 添加请求到队列
* @param {Object} request - { url, method, headers, body, priority, callback }
* priority: 0(最高) - 5(最低)
*/
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({
...request,
resolve,
reject
});
// 按优先级排序,优先级高的排在前面
this.queue.sort((a, b) => a.priority - b.priority);
this._process();
});
}
_process() {
// 如果还有请求且当前并发数未达上限,就继续处理
while (this.running < this.maxConcurrency && this.queue.length > 0) {
const task = this.queue.shift();
this.running++;
this._execute(task)
.then(task.resolve)
.catch(task.reject)
.finally(() => {
this.running--;
this._process(); // 继续处理队列中的下一个
});
}
}
async _execute(task) {
const options = {
method: task.method || 'GET',
headers: task.headers || {},
};
if (task.body) {
options.body = JSON.stringify(task.body);
options.headers['Content-Type'] = 'application/json';
}
const response = await fetch(task.url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${task.url}`);
}
return response.json();
}
}
// 使用示例
const requestQueue = new RequestQueue(6);
// 优先级0:核心数据必须最先获取
Promise.all([
requestQueue.add({
url: '/api/product/detail?id=12345',
priority: 0,
}),
requestQueue.add({
url: '/api/product/price?id=12345',
priority: 0,
}),
requestQueue.add({
url: '/api/product/stock?id=12345',
priority: 0,
}),
]).then(([detail, price, stock]) => {
console.log('核心数据加载完成', { detail, price, stock });
});
// 优先级1:推荐列表次之
requestQueue.add({
url: '/api/product/recommend?id=12345',
priority: 1,
}).then(recommendList => {
console.log('推荐列表加载完成', recommendList);
});
// 优先级3:评价和优惠券可以稍后
requestQueue.add({
url: '/api/user/coupon?id=12345',
priority: 3,
}).then(coupons => {
console.log('优惠券加载完成', coupons);
});
这个方案的好处
- 可控的并发数:你可以精确控制同时有多少请求在飞行
- 优先级调度:重要的数据先拿到,用户能更快看到核心内容
- 防泄漏:
.finally()确保计数正确归还,不会因为异常导致并发数一直不释放
解决方案二:关键请求预取与并行策略
思路转变
与其让所有请求排队,不如把”关键路径”上的请求单独提出来,用浏览器默认的高优先级发送,非关键的用自定义队列管理。
/**
* 智能请求分发器
* 核心数据走浏览器原生并发(快),非核心数据走队列(稳)
*/
class SmartRequestDispatcher {
constructor() {
this.preferentialQueue = []; // 浏览器原生请求,不限并发
this.managedQueue = new RequestQueue(4); // 自定义队列,限制4并发
}
/**
* 发送核心请求 - 利用浏览器原生并发能力
* 这些请求应该同时发出,不排队
*/
async sendCritical(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`请求失败: ${url}`);
return response.json();
}
/**
* 发送次要请求 - 走自定义队列
*/
async sendSecondary(request) {
return this.managedQueue.add(request);
}
/**
* 批量加载商品页面数据
*/
async loadProductPage(productId) {
// 第一步:关键数据并行加载(走浏览器默认并发)
const [detail, price, stock] = await Promise.all([
this.sendCritical(`/api/product/detail?id=${productId}`),
this.sendCritical(`/api/product/price?id=${productId}`),
this.sendCritical(`/api/product/stock?id=${productId}`),
]);
// 第二步:次要数据依次加载(走队列,不影响关键数据)
const [recommend, reviews, coupons] = await Promise.all([
this.sendSecondary({ url: `/api/product/recommend?id=${productId}`, priority: 1 }),
this.sendSecondary({ url: `/api/product/reviews?id=${productId}`, priority: 2 }),
this.sendSecondary({ url: `/api/user/coupon?id=${productId}`, priority: 2 }),
]);
return { detail, price, stock, recommend, reviews, coupons };
}
}
这个方案的精妙之处在于:关键数据直接”插队”,不走队列,而次要数据乖乖排队,既保证了用户体验,又不占用宝贵的并发资源。
解决方案三:请求合并与批量接口
问题根源
有时候并发限制被打爆,不是因为请求多,而是因为本该一次请求的数据被拆成了多次。
合并请求的实战写法
/**
* 请求合并器 - 将多个小请求合并成一个大请求
* 原理:后端提供一个批量接口,一次性返回所有数据
*/
class RequestMerger {
constructor() {
this.pendingRequests = new Map(); // requestId -> { resolve, reject, timeout }
this.batchInterval = 50; // 50ms内累积的请求合并发送
this.flushTimer = null;
}
/**
* 添加一个请求到合并队列
*/
enqueue(requestId, requestConfig) {
return new Promise((resolve, reject) => {
// 设置超时(避免请求永远挂起)
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error(`请求超时: ${requestId}`));
}, 5000);
this.pendingRequests.set(requestId, {
resolve,
reject,
config: requestConfig,
timeout
});
// 如果等待时间还没到,启动合并定时器
if (!this.flushTimer) {
this.flushTimer = setTimeout(() => this._flush(), this.batchInterval);
}
});
}
/**
* 批量发送累积的请求
*/
async _flush() {
this.flushTimer = null;
if (this.pendingRequests.size === 0) return;
// 收集所有待合并的请求
const requests = Array.from(this.pendingRequests.values());
const batchPayload = requests.map(req => ({
id: req.config.id,
url: req.config.url,
method: req.config.method,
body: req.config.body
}));
try {
// 一次性发批量请求
const response = await fetch('/api/product/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requests: batchPayload })
});
const results = await response.json();
// 将结果分发回各个Promise
requests.forEach(req => {
clearTimeout(req.timeout);
const result = results.find(r => r.id === req.config.id);
if (result && result.success) {
req.resolve(result.data);
} else {
req.reject(new Error(`批量请求失败: ${req.config.id}`));
}
});
} catch (error) {
// 批量请求失败,逐个reject
requests.forEach(req => {
clearTimeout(req.timeout);
req.reject(error);
});
}
}
}
// 使用示例
const merger = new RequestMerger();
// 前端同时发起多个"看似独立"的请求,实际后端只收到一个批量请求
const [detail, price, stock] = await Promise.all([
merger.enqueue('detail', { id: 'detail', url: '/api/product/detail', body: { productId: 12345 } }),
merger.enqueue('price', { id: 'price', url: '/api/product/price', body: { productId: 12345 } }),
merger.enqueue('stock', { id: 'stock', url: '/api/product/stock', body: { productId: 12345 } }),
]);
批量接口的后端实现参考
# Python Flask 示例:批量请求接口
from flask import Flask, request, jsonify
import asyncio
app = Flask(__name__)
@app.route('/api/product/batch', methods=['POST'])
def batch_product_data():
"""
批量接收前端请求,并行执行后端逻辑,一次性返回所有结果
"""
payload = request.json
requests = payload.get('requests', [])
results = []
# 根据请求类型分发到对应的处理函数
for req in requests:
req_id = req['id']
body = req.get('body', {})
product_id = body.get('productId')
# 这里可以用 asyncio.gather 并行执行多个异步查询
# 模拟不同数据源的查询
detail_task = query_product_detail(product_id) # 查商品详情
price_task = query_product_price(product_id) # 查实时价格
stock_task = query_product_stock(product_id) # 查库存
# 并行执行,节省时间
detail, price, stock = asyncio.run(
asyncio.gather(detail_task, price_task, stock_task)
)
results.append({
'id': req_id,
'success': True,
'data': {
'detail': detail,
'price': price,
'stock': stock
}
})
return jsonify(results)
async def query_product_detail(product_id):
# 查数据库...
return {"name": "iPhone 15 Pro", "brand": "Apple", ...}
async def query_product_price(product_id):
# 查价格数据库/缓存...
return {"currentPrice": 7999, "originalPrice": 8999, "discount": 11}
async def query_product_stock(product_id):
# 查库存系统...
return {"available": True, "count": 156}
这个方案的核心价值在于:前端看起来发了3个请求,实际后端只处理了1个HTTP连接,并发限制的问题直接消失。
解决方案四:请求去重与缓存策略
去重的重要性
在电商场景中,经常会出现同一个请求被多次触发的情况。比如用户快速点击推荐商品,或者页面滚动时重复触发加载逻辑。
/**
* 请求去重器
* 相同的请求只发一次,后续调用共享同一个Promise结果
*/
class DeduplicatedFetcher {
constructor() {
this.pendingRequests = new Map(); // url -> Promise
}
async fetch(url, options = {}) {
// 生成请求的唯一key(考虑方法、body等)
const cacheKey = this._generateKey(url, options);
// 如果已经有相同的请求在飞行中,直接共享结果
if (this.pendingRequests.has(cacheKey)) {
console.log(`[去重] 请求已在飞行中,复用结果: ${url}`);
return this.pendingRequests.get(cacheKey);
}
// 发起新请求,并缓存Promise
const promise = this._execute(url, options).finally(() => {
// 请求完成后(无论成功失败)从缓存中移除
this.pendingRequests.delete(cacheKey);
});
this.pendingRequests.set(cacheKey, promise);
return promise;
}
_generateKey(url, options) {
const method = options.method || 'GET';
const body = options.body ? JSON.stringify(options.body) : '';
return `${method}:${url}:${body}`;
}
async _execute(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return response.json();
}
}
// 使用示例
const dedupFetcher = new DeduplicatedFetcher();
// 即使同时发起多个相同请求,也只会实际发送一次
Promise.all([
dedupFetcher.fetch('/api/product/detail?id=12345'),
dedupFetcher.fetch('/api/product/detail?id=12345'), // 去重,复用上面的请求
dedupFetcher.fetch('/api/product/detail?id=12345'), // 去重,复用上面的请求
]).then(([result]) => {
console.log('只发了1次请求,3个调用共享结果', result);
});
结合本地缓存的完整方案
/**
* 带缓存的请求管理器
* 结合了去重、缓存、优先级和并发控制
*/
class SmartCacheManager {
constructor(options = {}) {
this.cache = new Map(); // 缓存存储
this.staleTime = options.staleTime || 5 * 60 * 1000; // 默认5分钟过期
this.requestQueue = new RequestQueue(options.maxConcurrency || 6);
this.dedupFetcher = new DeduplicatedFetcher();
}
/**
* 智能获取数据
* 优先级:缓存命中 > 去重请求 > 新请求
*/
async get(key, fetcher, options = {}) {
const { ttl, priority, forceRefresh = false } = options;
// 1. 检查缓存(除非强制刷新)
if (!forceRefresh) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < (ttl || this.staleTime)) {
console.log(`[缓存命中] ${key}`);
return cached.data;
}
// 缓存过期,但数据还在,可以先返回旧数据
if (cached) {
console.log(`[缓存过期] ${key},后台更新中...`);
// 后台静默更新,不阻塞当前渲染
this._backgroundUpdate(key, fetcher, ttl);
return cached.data;
}
}
// 2. 去重后发起请求
console.log(`[发起请求] ${key}`);
const data = await this.dedupFetcher.fetch(key, { priority });
// 3. 存入缓存
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttl || this.staleTime
});
return data;
}
async _backgroundUpdate(key, fetcher, ttl) {
try {
const data = await this.dedupFetcher.fetch(key);
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttl || this.staleTime
});
console.log(`[后台更新完成] ${key}`);
} catch (error) {
console.error(`[后台更新失败] ${key}:`, error);
}
}
/**
* 清除指定key的缓存
*/
invalidate(key) {
this.cache.delete(key);
console.log(`[缓存失效] ${key}`);
}
/**
* 清除所有缓存
*/
clearAll() {
this.cache.clear();
console.log('[缓存清除] 全部');
}
}
// 电商场景完整应用
const cacheManager = new SmartCacheManager({ maxConcurrency: 4 });
// 核心数据:带缓存,TTL 1分钟
const productDetail = await cacheManager.get(
'product:12345:detail',
() => fetch('/api/product/detail?id=12345').then(r => r.json()),
{ ttl: 60 * 1000, priority: 0 }
);
// 价格数据:TTL 30秒,因为价格变动频繁
const productPrice = await cacheManager.get(
'product:12345:price',
() => fetch('/api/product/price?id=12345').then(r => r.json()),
{ ttl: 30 * 1000, priority: 0 }
);
// 推荐列表:TTL 5分钟,变动不频繁
const recommendations = await cacheManager.get(
'product:12345:recommend',
() => fetch('/api/product/recommend?id=12345').then(r => r.json()),
{ ttl: 5 * 60 * 1000, priority: 1 }
);
解决方案五:使用HTTP/2和多路复用
了解HTTP/2的革命
如果你还没有用HTTP/2,那真的是亏大了。HTTP/2引入了多路复用(Multiplexing)机制,从根本上解决了HTTP/1.1的并发限制问题。
HTTP/1.1 的请求模式:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 请求1 │ │ 请求2 │ │ 请求3 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────┐
│ TCP连接(串行,一个接一个) │
└──────────────────────────────────────────┘
HTTP/2 的请求模式:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 请求1 │ │ 请求2 │ │ 请求3 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└──────────────┼──────────────┘
▼
┌──────────────────────────────────────────┐
│ TCP连接(多路复用,并行传输) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│
│ │流1 │ │流2 │ │流3 │ │流4 │ │流5 ││
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘│
└──────────────────────────────────────────┘
如何确认你的网站支持HTTP/2?
// 在前端检测当前连接的协议版本
if (window.location.protocol === 'https:') {
// 通过Performance API检测
const entries = performance.getEntriesByType('resource');
const https2Requests = entries.filter(e =>
e.transferSize > 0 &&
e.nextHopProtocol === 'h2'
);
console.log(`HTTP/2 请求数: ${https2Requests.length}`);
console.log(`HTTP/1.1 请求数: ${entries.length - https2Requests.length}`);
}
# Nginx 开启HTTP/2配置示例
server {
listen 443 ssl http2; # 加上http2关键字
server_name www.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HTTP/2 推荐配置
http2_push_preload on; # 服务端推送
location / {
root /var/www/html;
index index.html;
}
}
# Apache 开启HTTP/2配置
LoadModule http2_module modules/mod_http2.so
Protocols h2 h2c http/1.1
H2StreamConcurrency 100
有了HTTP/2,浏览器不再需要限制同一域名的并发连接数,因为多个请求可以在同一条TCP连接上并行传输,互不干扰。
解决方案六:Service Worker与后台同步
适用于特殊场景
对于电商网站来说,有些数据可以在用户浏览商品详情后,在后台悄悄更新推荐列表和价格数据,不需要用户等待。
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(registration => {
console.log('SW注册成功', registration);
// 尝试后台同步
if ('sync' in registration) {
registration.sync.register('update-product-data')
.then(() => console.log('后台同步注册成功'));
}
});
}
// sw.js - Service Worker代码
self.addEventListener('sync', event => {
if (event.tag === 'update-product-data') {
event.waitUntil(
// 后台并行更新多个数据源
Promise.all([
fetch('/api/product/recommend?id=12345')
.then(res => res.json())
.then(data => caches.open('product-cache').then(cache =>
cache.put('/recommend', new Response(JSON.stringify(data)))
)),
fetch('/api/product/price?id=12345')
.then(res => res.json())
.then(data => caches.open('product-cache').then(cache =>
cache.put('/price', new Response(JSON.stringify(data)))
)),
])
);
}
});
完整实战方案:电商商品页请求优化架构
架构总览
┌─────────────────────────────────────────────────────┐
│ 前端请求分发层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 关键路径请求 │ │ 次要路径请求 │ │ 预加载请求 │ │
│ │ (detail/ │ │ (recommend │ │ (images/ │ │
│ │ price/ │ │ /review) │ │ related) │ │
│ │ stock) │ │ │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 并发控制 & 去重管理器 │ │
│ │ • 同一请求只发一次 │ │
│ │ • 核心请求最高优先级 │ │
│ │ • 非核心请求排队处理 │ │
│ └────────────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 请求合并层(批量接口) │ │
│ │ 50ms内累积的请求 → 合并为1个批量请求 │ │
│ └────────────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 缓存 & Service Worker层 │ │
│ │ • 命中缓存直接返回 │ │
│ │ • 过期缓存后台静默更新 │ │
│ │ • SW做离线缓存和后台同步 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
完整代码实现
/**
* 电商商品页请求管理器 - 完整版
* 整合了优先级队列、去重、缓存、批量合并
*/
class ProductPageRequestManager {
constructor(productId) {
this.productId = productId;
this.cache = new Map();
this.pendingRequests = new Map();
this.batchBuffer = [];
this.batchTimer = null;
this.BATCH_WINDOW = 50; // 50ms批量窗口
this.MAX_CONCURRENCY = 4;
this.activeRequests = 0;
}
/**
* 获取商品详情数据(最高优先级)
*/
async getDetail() {
return this._smartRequest(`detail:${this.productId}`,
() => this._sendRequest(`/api/product/detail?id=${this.productId}`),
{ priority: 0, ttl: 60000 }
);
}
/**
* 获取商品实时价格(最高优先级)
*/
async getPrice() {
return this._smartRequest(`price:${this.productId}`,
() => this._sendRequest(`/api/product/price?id=${this.productId}`),
{ priority: 0, ttl: 30000 }
);
}
/**
* 获取库存信息(最高优先级)
*/
async getStock() {
return this._smartRequest(`stock:${this.productId}`,
() => this._sendRequest(`/api/product/stock?id=${this.productId}`),
{ priority: 0, ttl: 30000 }
);
}
/**
* 获取推荐列表(次高优先级)
*/
async getRecommendations() {
return this._smartRequest(`recommend:${this.productId}`,
() => this._sendRequest(`/api/product/recommend?id=${this.productId}`),
{ priority: 1, ttl: 300000 }
);
}
/**
* 获取商品评价(低优先级)
*/
async getReviews(page = 1) {
return this._smartRequest(`reviews:${this.productId}:${page}`,
() => this._sendRequest(`/api/product/reviews?id=${this.productId}&page=${page}`),
{ priority: 2, ttl: 600000 }
);
}
/**
* 智能请求分发器 - 核心方法
*/
async _smartRequest(cacheKey, fetcher, options = {}) {
const { ttl = 300000, priority = 2 } = options;
// 1. 检查缓存
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log(`[缓存命中] ${cacheKey}`);
return cached.data;
}
// 2. 去重:相同请求只在飞行一次
if (this.pendingRequests.has(cacheKey)) {
console.log(`[去重] ${cacheKey},等待同请求结果`);
return this.pendingRequests.get(cacheKey);
}
// 3. 并发控制:等有空闲槽位
await this._waitForSlot();
// 4. 发起请求
console.log(`[发起请求] ${cacheKey} (优先级: ${priority})`);
const requestPromise = this._executeWithRetry(fetcher, cacheKey, 3);
// 缓存Promise(用于去重)
this.pendingRequests.set(cacheKey, requestPromise);
try {
const data = await requestPromise;
// 存入缓存
this.cache.set(cacheKey, { data, timestamp: Date.now(), ttl });
return data;
} catch (error) {
console.error(`[请求失败] ${cacheKey}:`, error);
throw error;
} finally {
// 请求完成后释放槽位
this.pendingRequests.delete(cacheKey);
this._releaseSlot();
}
}
/**
* 带重试的请求执行
*/
async _executeWithRetry(fetcher, cacheKey, maxRetries) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fetcher();
} catch (error) {
if (attempt === maxRetries) throw error;
console.warn(`[重试] ${cacheKey} 第${attempt}次失败,${maxRetries - attempt}次重试机会剩余`);
await this._delay(1000 * attempt); // 指数退避
}
}
}
/**
* 发送请求(加入批量合并)
*/
async _sendRequest(url) {
return new Promise((resolve, reject) => {
const request = { url, resolve, reject };
// 加入批量缓冲区
this.batchBuffer.push(request);
// 如果缓冲区积累到了,或者这是最后一个请求,就批量发送
if (this.batchBuffer.length >= 3 || this.activeRequests >= this.MAX_CONCURRENCY) {
this._flushBatch();
} else if (!this.batchTimer) {
// 否则等一个批量窗口
this.batchTimer = setTimeout(() => this._flushBatch(), this.BATCH_WINDOW);
}
});
}
/**
* 批量发送请求
*/
async _flushBatch() {
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
if (this.batchBuffer.length === 0) return;
const batch = this.batchBuffer.splice(0);
try {
const response = await fetch('/api/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: this.productId,
requests: batch.map(req => ({ url: req.url }))
})
});
const results = await response.json();
// 分发结果
batch.forEach(req => {
const result = results.find(r => r.url === req.url);
if (result && result.success) {
req.resolve(result.data);
} else {
req.reject(new Error(`批量请求失败: ${req.url}`));
}
});
} catch (error) {
batch.forEach(req => req.reject(error));
}
}
/**
* 并发控制 - 等待可用槽位
*/
_waitForSlot() {
return new Promise(resolve => {
const check = () => {
if (this.activeRequests < this.MAX_CONCURRENCY) {
this.activeRequests++;
resolve();
} else {
setTimeout(check, 10);
}
};
check();
});
}
_releaseSlot() {
this.activeRequests = Math.max(0, this.activeRequests - 1);
}
_delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 页面卸载时清理
*/
destroy() {
if (this.batchTimer) clearTimeout(this.batchTimer);
this.batchBuffer = [];
this.cache.clear();
this.pendingRequests.clear();
}
}
// ========== 使用示例 ==========
async function loadProductPage(productId) {
const manager = new ProductPageRequestManager(productId);
try {
// 第一步:等待核心数据(detail/price/stock)
const [detail, price, stock] = await Promise.all([
manager.getDetail(),
manager.getPrice(),
manager.getStock(),
]);
// 渲染核心信息
renderProductCore({ detail, price, stock });
// 第二步:核心数据渲染后,异步加载推荐和评价
manager.getRecommendations().then(recommendations => {
renderRecommendations(recommendations);
});
manager.getReviews(1).then(reviews => {
renderReviews(reviews);
});
return { detail, price, stock };
} finally {
manager.destroy();
}
}
function renderProductCore({ detail, price, stock }) {
document.getElementById('product-name').textContent = detail.name;
document.getElementById('product-price').textContent = `¥${price.currentPrice}`;
document.getElementById('product-stock').textContent = stock.available
? `库存: ${stock.count}件`
: '暂时缺货';
}
function renderRecommendations(recommendations) {
const container = document.getElementById('recommendations');
container.innerHTML = recommendations.map(product => `
<div class="recommend-item" data-id="${product.id}">
<img src="${product.thumbnail}" alt="${product.name}">
<p>${product.name}</p>
<span class="price">¥${product.price}</span>
</div>
`).join('');
}
function renderReviews(reviews) {
const container = document.getElementById('reviews');
container.innerHTML = reviews.items.map(review => `
<div class="review-item">
<strong>${review.userName}</strong>
<span class="rating">★${review.rating}</span>
<p>${review.content}</p>
</div>
`).join('');
}
// 页面加载时调用
loadProductPage(12345);
各方案的适用场景总结
| 方案 | 适用场景 | 实施难度 | 效果 |
|---|---|---|---|
| 请求优先级队列 | 通用,几乎所有场景 | ⭐ | ⭐⭐⭐ |
| 关键/次要请求分离 | 有明确数据优先级时 | ⭐⭐ | ⭐⭐⭐⭐ |
| 请求合并/批量接口 | 后端可控时 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 请求去重 | 可能有重复请求时 | ⭐ | ⭐⭐⭐ |
| HTTP/2 | 可以部署HTTPS时 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| Service Worker | 需要离线/后台同步 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
最后的小建议
- 监控你的请求:打开浏览器开发者工具的Network面板,观察实际发出的请求数量和顺序
- 不要用
async/await串行请求:除非有依赖关系,否则用Promise.all并行 - 给请求设置超时:避免请求永远挂起占用并发槽位
- 后端能合并的就合并:前端的优化有上限,后端的批量接口才是真正的降维打击
- 考虑CDN和边缘计算:推荐列表这类数据变动不大的内容,完全可以放到CDN上
希望这篇指南能帮你彻底搞定电商网站的AJAX并发问题。如果有具体的场景需要讨论,随时来找我!
