logo

前端并发请求数量控制:策略与实现方案详解

作者:蛮不讲李2025.09.26 15:35浏览量:2

简介:本文深入探讨前端并发请求数量控制的必要性,介绍限制并发请求的常见策略,如信号量模式、任务队列、令牌桶算法等,并提供具体实现代码与优化建议。

前端并发请求数量控制:策略与实现方案详解

一、为什么需要控制并发请求数量?

在前端开发中,浏览器对同一域名下的并发请求数量存在限制(通常为6-8个),这是由HTTP协议和浏览器实现机制决定的。当业务场景需要发起大量异步请求时(如批量数据上传、多API并行调用),超出浏览器限制的并发请求会导致:

  1. 请求阻塞:后续请求被强制排队,延长整体响应时间
  2. 资源竞争:CPU/内存占用激增,可能引发页面卡顿
  3. 错误率上升网络拥塞导致请求超时或失败
  4. 不可预测的行为:不同浏览器的并发限制差异导致表现不一致

典型应用场景包括:

  • 批量文件上传(需控制同时上传的文件数)
  • 数据仪表盘(同时加载多个独立数据源)
  • 微前端架构(子应用独立发起请求)
  • 测试工具模拟高并发场景

二、核心控制策略与实现方案

1. 信号量模式(Semaphore Pattern)

原理:通过计数器跟踪正在执行的请求数,当达到阈值时阻塞新请求。

  1. class RequestSemaphore {
  2. constructor(maxConcurrent = 5) {
  3. this.maxConcurrent = maxConcurrent;
  4. this.activeCount = 0;
  5. this.queue = [];
  6. }
  7. async execute(requestFn) {
  8. if (this.activeCount >= this.maxConcurrent) {
  9. return new Promise((resolve) => {
  10. this.queue.push({ requestFn, resolve });
  11. });
  12. }
  13. this.activeCount++;
  14. try {
  15. const result = await requestFn();
  16. this._dequeue();
  17. return result;
  18. } catch (error) {
  19. this._dequeue();
  20. throw error;
  21. }
  22. }
  23. _dequeue() {
  24. this.activeCount--;
  25. if (this.queue.length > 0) {
  26. const { requestFn, resolve } = this.queue.shift();
  27. resolve(this.execute(requestFn));
  28. }
  29. }
  30. }
  31. // 使用示例
  32. const semaphore = new RequestSemaphore(3);
  33. const mockRequest = (id) => () =>
  34. new Promise(resolve =>
  35. setTimeout(() => resolve(`Response ${id}`), 1000)
  36. );
  37. // 并发执行5个请求,但实际同时只有3个在运行
  38. for (let i = 1; i <= 5; i++) {
  39. semaphore.execute(mockRequest(i)).then(console.log);
  40. }

优化点

  • 添加超时机制防止队列堆积
  • 支持优先级队列(紧急请求优先)
  • 添加请求取消功能

2. 任务队列模式(Task Queue)

原理:将所有请求放入队列,按固定批次执行。

  1. class BatchRequestQueue {
  2. constructor(batchSize = 3, batchInterval = 0) {
  3. this.batchSize = batchSize;
  4. this.batchInterval = batchInterval;
  5. this.queue = [];
  6. this.timer = null;
  7. }
  8. add(requestFn) {
  9. this.queue.push(requestFn);
  10. if (!this.timer && this.queue.length >= this.batchSize) {
  11. this._processBatch();
  12. } else if (this.batchInterval > 0 && !this.timer) {
  13. this.timer = setTimeout(() => this._processBatch(), this.batchInterval);
  14. }
  15. }
  16. async _processBatch() {
  17. const batch = this.queue.splice(0, this.batchSize);
  18. const results = await Promise.all(batch.map(fn => fn()));
  19. // 处理结果...
  20. if (this.queue.length > 0 && !this.timer) {
  21. this.timer = setTimeout(() => this._processBatch(), this.batchInterval);
  22. }
  23. }
  24. }

适用场景

  • 需要严格控制请求发起节奏
  • 请求间无强依赖关系
  • 批量处理效率更高

3. 令牌桶算法(Token Bucket)

原理:以固定速率生成令牌,每个请求需要消耗一个令牌。

  1. class TokenBucket {
  2. constructor(capacity = 5, refillRate = 1) {
  3. this.capacity = capacity;
  4. this.tokens = capacity;
  5. this.refillRate = refillRate; // tokens per second
  6. this.lastRefillTime = Date.now();
  7. this.queue = [];
  8. }
  9. _refill() {
  10. const now = Date.now();
  11. const elapsed = (now - this.lastRefillTime) / 1000;
  12. const newTokens = elapsed * this.refillRate;
  13. this.tokens = Math.min(this.capacity, this.tokens + newTokens);
  14. this.lastRefillTime = now;
  15. }
  16. async consume() {
  17. this._refill();
  18. if (this.tokens >= 1) {
  19. this.tokens -= 1;
  20. return Promise.resolve();
  21. }
  22. return new Promise(resolve => {
  23. this.queue.push(resolve);
  24. const checkTokens = () => {
  25. this._refill();
  26. if (this.tokens >= 1 && this.queue.length > 0) {
  27. this.tokens -= 1;
  28. const resolveFn = this.queue.shift();
  29. resolveFn();
  30. } else {
  31. setTimeout(checkTokens, 100);
  32. }
  33. };
  34. setTimeout(checkTokens, 100);
  35. });
  36. }
  37. }
  38. // 使用示例
  39. const bucket = new TokenBucket(3, 0.5); // 初始3个令牌,每秒补充0.5个
  40. async function makeRequest() {
  41. await bucket.consume();
  42. return fetch('https://api.example.com/data');
  43. }

优势

  • 平滑突发流量
  • 精确控制请求速率
  • 避免全局阻塞

三、进阶优化技巧

1. 请求优先级管理

  1. class PriorityRequestQueue {
  2. constructor(maxConcurrent = 3) {
  3. this.maxConcurrent = maxConcurrent;
  4. this.activeCount = 0;
  5. this.highPriorityQueue = [];
  6. this.lowPriorityQueue = [];
  7. }
  8. async add(requestFn, isHighPriority = false) {
  9. const queue = isHighPriority ? this.highPriorityQueue : this.lowPriorityQueue;
  10. queue.push(requestFn);
  11. this._processQueue();
  12. }
  13. async _processQueue() {
  14. if (this.activeCount >= this.maxConcurrent) return;
  15. let nextRequest;
  16. if (this.highPriorityQueue.length > 0) {
  17. nextRequest = this.highPriorityQueue.shift();
  18. } else if (this.lowPriorityQueue.length > 0) {
  19. nextRequest = this.lowPriorityQueue.shift();
  20. }
  21. if (nextRequest) {
  22. this.activeCount++;
  23. try {
  24. const result = await nextRequest();
  25. this.activeCount--;
  26. this._processQueue();
  27. return result;
  28. } catch (error) {
  29. this.activeCount--;
  30. this._processQueue();
  31. throw error;
  32. }
  33. }
  34. }
  35. }

2. 动态调整并发数

根据网络状况动态调整并发数:

  1. function detectNetworkCondition() {
  2. return new Promise(resolve => {
  3. const startTime = performance.now();
  4. const img = new Image();
  5. img.onload = () => {
  6. const duration = performance.now() - startTime;
  7. if (duration < 100) resolve('fast'); // 快速网络
  8. else if (duration < 500) resolve('medium'); // 中等网络
  9. else resolve('slow'); // 慢速网络
  10. };
  11. img.src = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz4=`;
  12. });
  13. }
  14. async function getAdaptiveConcurrency() {
  15. const network = await detectNetworkCondition();
  16. const map = { fast: 10, medium: 5, slow: 2 };
  17. return map[network] || 3;
  18. }

3. 请求重试机制

  1. async function retryableRequest(requestFn, maxRetries = 3) {
  2. let lastError;
  3. for (let i = 0; i <= maxRetries; i++) {
  4. try {
  5. return await requestFn();
  6. } catch (error) {
  7. lastError = error;
  8. if (i === maxRetries) throw error;
  9. await new Promise(resolve =>
  10. setTimeout(resolve, 1000 * Math.pow(2, i)) // 指数退避
  11. );
  12. }
  13. }
  14. throw lastError;
  15. }

四、最佳实践建议

  1. 合理设置初始并发数

    • 移动端建议2-4个
    • 桌面端建议4-8个
    • 通过navigator.connection.effectiveType检测网络类型动态调整
  2. 请求分类管理

    • 关键路径请求优先
    • 非关键请求延迟或合并
  3. 监控与调优

    1. function monitorRequests() {
    2. const performanceObserver = new PerformanceObserver((list) => {
    3. for (const entry of list.getEntries()) {
    4. if (entry.name.includes('fetch')) {
    5. console.log(`Request ${entry.name} took ${entry.duration}ms`);
    6. }
    7. }
    8. });
    9. performanceObserver.observe({ entryTypes: ['resource'] });
    10. }
  4. 与后端协同

    • 通过HTTP头X-Request-ID追踪请求
    • 协商合理的批处理接口
  5. 渐进增强策略

    1. async function smartFetch(url, options = {}) {
    2. const supportsConcurrent = 'signal' in new Request('');
    3. if (supportsConcurrent && options.concurrency) {
    4. return usingConcurrentStrategy(url, options);
    5. }
    6. return fallbackFetch(url, options);
    7. }

五、常见误区与解决方案

  1. 误区:认为Promise.all()可以控制并发
    事实:它只是等待所有Promise完成,不限制同时执行的Promise数量
    解决方案:使用上述控制策略包装Promise.all()

  2. 误区:设置过高的并发数可以提升性能
    事实:超过浏览器限制会导致请求排队,反而降低性能
    解决方案:通过真实用户监控(RUM)确定最优并发数

  3. 误区:忽略请求取消功能
    事实:用户导航离开页面时,未完成的请求会浪费资源
    解决方案:使用AbortController实现请求取消

  1. class CancellableRequestQueue {
  2. constructor(maxConcurrent = 3) {
  3. this.maxConcurrent = maxConcurrent;
  4. this.activeRequests = new Set();
  5. }
  6. async add(requestFn) {
  7. if (this.activeRequests.size >= this.maxConcurrent) {
  8. throw new Error('Queue full');
  9. }
  10. const controller = new AbortController();
  11. const signal = controller.signal;
  12. this.activeRequests.add(controller);
  13. try {
  14. const result = await requestFn({ signal });
  15. this.activeRequests.delete(controller);
  16. return result;
  17. } catch (error) {
  18. if (error.name !== 'AbortError') {
  19. this.activeRequests.delete(controller);
  20. }
  21. throw error;
  22. }
  23. }
  24. cancelAll() {
  25. this.activeRequests.forEach(ctrl => ctrl.abort());
  26. this.activeRequests.clear();
  27. }
  28. }

六、未来演进方向

  1. Web Streams API

    • 使用ReadableStream处理大数据流,减少内存占用
    • 示例:分块上传文件时控制并发块数量
  2. Service Worker中介

    • 在Service Worker层统一控制请求并发
    • 实现跨标签页的请求协调
  3. HTTP/2多路复用

    • 虽然HTTP/2允许单个连接多路复用,但仍需控制应用层并发
    • 结合Server Push优化关键资源加载
  4. WebTransport提案

    • 为需要低延迟的场景提供更细粒度的控制

通过系统化的并发请求控制,前端应用可以在保证用户体验的同时,更高效地利用网络资源。建议开发者根据具体业务场景,选择或组合适合的控制策略,并通过持续监控不断优化参数设置。

相关文章推荐

发表评论

活动