async vs queue-promise vs bottleneck vs p-queue vs promise-queue
JavaScript 异步任务队列与并发控制方案对比
asyncqueue-promisebottleneckp-queuepromise-queue类似的npm包:

JavaScript 异步任务队列与并发控制方案对比

asyncbottleneckp-queuepromise-queuequeue-promise 都是用于管理 JavaScript 异步任务执行顺序和并发数量的工具。async 是老牌控制流库,提供队列功能但基于回调;bottleneck 专注于速率限制和并发控制,支持分布式场景;p-queue 是现代 Promise 原生队列,轻量且类型友好;promise-queuequeue-promise 则是更简单的实现,适合基础需求。它们在 API 设计、维护状态和功能深度上存在显著差异。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
async84,616,03228,198808 kB242 年前MIT
queue-promise39,4559229.2 kB13-MIT
bottleneck01,977-897 年前MIT
p-queue04,14076.8 kB52 个月前MIT
promise-queue0230-108 年前MIT

JavaScript 异步任务队列与并发控制方案对比

在构建高可用的前端或 Node.js 应用时,控制异步任务的并发数量至关重要。无论是为了避免浏览器卡死、防止 API 触发速率限制,还是为了管理资源密集型操作,我们都需要可靠的队列机制。asyncbottleneckp-queuepromise-queuequeue-promise 都试图解决这个问题,但它们的侧重点和适用场景截然不同。

🏗️ 核心机制:回调 vs Promise vs 限流

async 是老牌的控制流库,它的队列功能基于回调风格,虽然支持 Promise,但基因里带着旧时代的印记。

// async: 基于回调的队列
const queue = async.queue((task, callback) => {
  fetchData(task).then(res => callback(null, res));
}, 2); // 并发数为 2

queue.push({ id: 1 });

bottleneck 的核心是限流器(Limiter),它不仅控制并发,还控制时间间隔。

// bottleneck: 专注于限流
const limiter = new Bottleneck({ maxConcurrent: 5, minTime: 100 });

limiter.schedule(() => fetchData());

p-queue 是纯粹的 Promise 队列,设计现代,默认挂起直到添加任务。

// p-queue: Promise 原生
const queue = new PQueue({ concurrency: 5 });

queue.add(() => fetchData());

promise-queue 提供基础的 Promise 排队功能,API 较为简单。

// promise-queue: 基础实现
const queue = new PromiseQueue(5, 100);

queue.add(fetchData());

queue-promise 同样专注于 Promise 队列,强调简洁性。

// queue-promise: 简洁实现
const queue = new Queue();

queue.enqueue(() => fetchData());

🚦 并发与速率控制:数量 vs 时间

这是选择库时的关键分水岭。如果你只关心"同时跑几个",大多数库都能做。如果你关心"每秒跑几个",选择范围会缩小。

  • async:仅支持并发数(concurrency)。无法直接设置时间间隔。
  • p-queue:支持并发数。可以通过 interval 选项实现简单的速率控制,但不如 bottleneck 精细。
  • bottleneck:同时支持并发数(maxConcurrent)和时间间隔(minTime)。它是唯一原生支持令牌桶算法的库。
  • promise-queue:主要支持并发数。
  • queue-promise:主要支持并发数。
// p-queue: 简单的间隔控制
const queue = new PQueue({ concurrency: 1, interval: 1000 });

// bottleneck: 精细的限流
const limiter = new Bottleneck({ 
  maxConcurrent: 5, 
  minTime: 200 // 任务之间至少间隔 200ms
});

🛡️ 错误处理与任务状态

在生产环境中,任务失败是常态。如何处理失败的任务,以及能否监听队列状态,直接影响系统的稳定性。

async 通过回调的 error 参数传递错误,需要手动包装 Promise。

// async: 错误通过回调传递
const queue = async.queue((task, callback) => {
  task().then(res => callback(null, res)).catch(err => callback(err));
});

queue.error((err) => console.error(err));

p-queue 直接返回 Promise,错误会 reject,同时提供事件监听。

// p-queue: 原生 Promise 错误处理
queue.add(() => fetchData()).catch(err => console.error(err));

queue.on('error', (err) => console.error(err));

bottleneck 提供丰富的事件系统,包括失败重试逻辑。

// bottleneck: 丰富的事件监听
limiter.on('failed', (error, jobInfo) => {
  console.log('Task failed', error);
});

promise-queuequeue-promise 通常通过 Promise 的 catch 捕获错误,事件系统较少。

// promise-queue / queue-promise: 基础错误捕获
queue.add(fetchData()).catch(err => console.error(err));

📦 维护状态与生态风险

在选择依赖时,库的维护活跃度是隐形成本。

  • async:维护良好,但属于"遗留技术"。新项目除非已有依赖,否则不建议引入整个库只为用队列。
  • bottleneck:维护活跃,功能复杂,适合后端或重度 API 调用场景。
  • p-queue:维护非常活跃,是 sindresorhus 生态的一部分,TypeScript 支持最好。
  • promise-queue:更新频率较低,功能简单,适合非核心场景。
  • queue-promise:社区规模较小,长期维护性不如 p-queue

⚠️ 注意:对于新项目,promise-queuequeue-promise 虽然没有被官方标记为废弃,但由于 p-queue 提供了更好的类型支持和活跃度,通常建议优先选择 p-queue

🌐 真实场景选型指南

场景 1:调用第三方 API,防止触发 429 限流

  • 最佳选择bottleneck
  • 理由:需要精确控制时间间隔(如每 100ms 一个请求),而不仅仅是并发数。
const limiter = new Bottleneck({ maxConcurrent: 5, minTime: 100 });
const results = await Promise.all(urls.map(url => limiter.schedule(() => fetch(url))));

场景 2:前端图片批量上传,限制同时上传 5 张

  • 最佳选择p-queue
  • 理由:Promise 原生,API 简洁,易于与 React/Vue 集成。
const queue = new PQueue({ concurrency: 5 });
files.forEach(file => queue.add(() => upload(file)));
await queue.onIdle();

场景 3:遗留 Node.js 项目,已有 async 依赖

  • 最佳选择async
  • 理由:避免引入新依赖,保持代码风格一致。
const q = async.worker((task, cb) => { /*...*/ }, 5);
q.push(tasks);

场景 4:极简脚本,只需顺序执行

  • 最佳选择queue-promisepromise-queue
  • 理由:依赖体积小,功能足够。
const queue = new Queue();
queue.enqueue(task1);
queue.enqueue(task2);

📊 核心特性对比表

特性asyncbottleneckp-queuepromise-queuequeue-promise
核心风格回调 (Callback)限流器 (Limiter)Promise 原生Promise 基础Promise 基础
并发控制
速率限制✅ (强)⚠️ (弱)
TS 支持⚠️ (一般)✅ (优秀)⚠️⚠️
维护状态稳定 ( legacy)活跃活跃低频低频
适用场景旧项目/复杂流API 限流/分布式现代通用队列简单脚本简单脚本

💡 架构师建议

在现代前端架构中,p-queue 是默认的首选。它在功能、体积和维护性之间取得了最佳平衡。它的 API 设计符合现代 JavaScript 开发者的直觉,且 TypeScript 支持完善,能减少运行时错误。

bottleneck 是特定场景的王者。当你面对严格的 API 速率限制(如 Google Maps API、GitHub API)时,不要尝试用 p-queue 去模拟限流逻辑,直接使用 bottleneck 会更安全、更可靠。

async 应逐步淘汰。除非你正在维护一个深度依赖 async 控制流(如 async.waterfall)的旧系统,否则在新代码中应避免引入它。原生 async/await 配合 p-queue 能提供更清晰的代码结构。

promise-queuequeue-promise 需谨慎。它们没有明显的功能缺陷,但生态活跃度远不如 p-queue。在长期维护的企业级项目中,选择社区更活跃的库能降低未来的迁移成本。

🔚 总结

  • 需要速率限制?选 bottleneck
  • 需要通用并发队列?选 p-queue
  • 维护旧代码?用 async
  • 追求极简?考虑 queue-promise,但需评估风险。

选择正确的工具不仅能解决当前的并发问题,还能让代码在未来更容易维护和扩展。

如何选择: async vs queue-promise vs bottleneck vs p-queue vs promise-queue

  • async:

    如果你的项目遗留了大量回调代码,或者需要复杂的控制流(如 waterfall、series 而不仅是队列),选择 async。它是一个功能全面的工具库,但在新项目中因其回调风格较重,通常不如 Promise 原生库推荐。

  • queue-promise:

    类似于 promise-queuequeue-promise 适合非常基础的入队出队需求。如果项目不需要复杂的错误处理或优先级队列,且希望依赖尽可能小,可以使用它,但建议优先评估 p-queue

  • bottleneck:

    如果你需要严格的速率限制(例如调用 API 时每秒不超过 5 次),而不仅仅是并发控制,选择 bottleneck。它支持 Redis 后端,适合分布式节点间的限流场景,功能最强大但也最重。

  • p-queue:

    对于大多数现代前端或 Node.js 项目,选择 p-queue。它是 Promise 原生的,维护活跃,类型定义完善,且 API 简洁。它是 sindresorhus 生态的一部分,适合需要可靠并发控制的通用场景。

  • promise-queue:

    如果你需要一个极简单的队列实现,且不需要额外的并发配置或事件监听,可以考虑 promise-queue。但需注意其社区活跃度较低,适合内部工具或对依赖体积极其敏感的非核心场景。

async的README

Async Logo

Github Actions CI status NPM version Coverage Status Join the chat at https://gitter.im/caolan/async jsDelivr Hits

Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript. Although originally designed for use with Node.js and installable via npm i async, it can also be used directly in the browser. An ESM/MJS version is included in the main async package that should automatically be used with compatible bundlers such as Webpack and Rollup.

A pure ESM version of Async is available as async-es.

For Documentation, visit https://caolan.github.io/async/

For Async v1.5.x documentation, go HERE

// for use with Node-style callbacks...
var async = require("async");

var obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"};
var configs = {};

async.forEachOf(obj, (value, key, callback) => {
    fs.readFile(__dirname + value, "utf8", (err, data) => {
        if (err) return callback(err);
        try {
            configs[key] = JSON.parse(data);
        } catch (e) {
            return callback(e);
        }
        callback();
    });
}, err => {
    if (err) console.error(err.message);
    // configs is now a map of JSON data
    doSomethingWith(configs);
});
var async = require("async");

// ...or ES2017 async functions
async.mapLimit(urls, 5, async function(url) {
    const response = await fetch(url)
    return response.body
}, (err, results) => {
    if (err) throw err
    // results is now an array of the response bodies
    console.log(results)
})