async vs queue-promise vs bottleneck vs p-queue vs promise-queue
JavaScript 非同期タスクキューとレート制限ライブラリの比較
asyncqueue-promisebottleneckp-queuepromise-queue類似パッケージ:

JavaScript 非同期タスクキューとレート制限ライブラリの比較

asyncbottleneckp-queuepromise-queuequeue-promise は、JavaScript 環境における非同期処理の制御、タスクのキューイング、および API レート制限を管理するためのライブラリです。async は最も歴史が古く、コレクション操作やフロー制御を含む汎用非同期ユーティリティを提供します。bottleneck はレート制限と負荷分散に特化しており、API 呼び出しの制限を厳密に守りたい場合に適しています。p-queue は Promise ベースのシンプルで軽量なキュー実装で、並行処理数の制御に優れています。promise-queuequeue-promise はよりシンプルなキュー実装ですが、メンテナンス状況や機能面で違いがあります。これらはすべて、ブラウザと Node.js の両方で動作しますが、モダンな Promise ベースのプロジェクトでは p-queuebottleneck が推奨される傾向にあります。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
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 はそれぞれ異なるアプローチでこの問題に対処します。本記事では、実務での選択基準を明確にするために、これら 5 つのパッケージを技術的に深く比較します。

🏗️ アーキテクチャと設計思想

各ライブラリは異なる設計思想を持っており、それが機能セットと使用场景に影響します。

async は 2010 年頃から存在する最も歴史のあるライブラリで、非同期コレクション操作とフロー制御を包括的に提供します。Promise 以前のコールバック時代から進化し、現在は Promise ベースの API も提供しています。

// async: 非同期 map 処理
import { map } from 'async';

const results = await map(['file1.txt', 'file2.txt'], async (file) => {
  return await readFile(file);
});

bottleneck はレート制限と負荷分散に特化したライブラリです。単なるキューではなく、時間ベースの制限(例:1 秒間に 5 リクエスト)を厳密に管理します。

// bottleneck: レート制限付き実行
import Bottleneck from 'bottleneck';

const limiter = new Bottleneck({ minTime: 200, maxConcurrent: 5 });

const result = await limiter.schedule(() => fetch('/api/data'));

p-queue は Promise ベースの軽量キューで、並行処理数の制御に焦点を当てています。Sindre Sorhus 氏によってメンテナンスされており、モダンな JavaScript プロジェクトとの親和性が高いです。

// p-queue: 並行処理制限
import PQueue from 'p-queue';

const queue = new PQueue({ concurrency: 5 });

await queue.add(() => fetch('/api/item/1'));
await queue.add(() => fetch('/api/item/2'));

promise-queuequeue-promise はよりシンプルな FIFO キュー実装で、機能は限定的ですが、特定のユースケースでは十分です。

// promise-queue: 基本的なキュー
import PromiseQueue from 'promise-queue';

const queue = new PromiseQueue(5, 1); // 5 並行、無限待機

queue.add(() => fetch('/api/data'));
// queue-promise: シンプル実装
import Queue from 'queue-promise';

const queue = new Queue();

queue.add(() => fetch('/api/data'));

⚡ 並行処理制御の仕組み

並行処理数を制限する方法は、ライブラリによって大きく異なります。

パッケージ並行制御レート制限優先度分散対応
async❌ (mapSeries 等)
bottleneck✅ (Redis)
p-queue
promise-queue
queue-promise

async は並行処理制御というより、コレクション操作の非同期バージョンを提供します。parallelserieswaterfall などのフロー制御関数がありますが、動的な並行数制限はできません。

// async: 並列実行(全タスク同時開始)
import { parallel } from 'async';

await parallel([
  (cb) => setTimeout(() => cb(null, 1), 100),
  (cb) => setTimeout(() => cb(null, 2), 100)
]);

bottleneck は最も高度な制御を提供し、minTime(タスク間の最小時間)と maxConcurrent(最大並行数)の両方を設定できます。

// bottleneck: 詳細なレート制限
const limiter = new Bottleneck({
  maxConcurrent: 5,      // 最大 5 並列
  minTime: 100,          // タスク間 100ms 以上
  highWater: 100,        // キューの警告閾値
  strategy: Bottleneck.strategy.OVERFLOW // 超過時の戦略
});

p-queueconcurrency オプションで並行処理数を制限しますが、時間ベースのレート制限は built-in で提供していません。

// p-queue: 並行数制限のみ
const queue = new PQueue({ 
  concurrency: 5,
  interval: 1000,        // 追加機能:時間ベース制限も可能
  intervalCap: 10        // 1 秒間に 10 タスク
});

promise-queue はコンストラクタで並行数と待機時間を指定しますが、柔軟性は低いです。

// promise-queue: 固定設定
const queue = new PromiseQueue(
  5,    // 並行処理数
  1000  // 待機時間(使用されない場合あり)
);

queue-promise は最もシンプルで、並行処理制御機能は限定的または存在しません。

// queue-promise: 基本的なキュー
const queue = new Queue();
// 並行制御オプションは限定的

🔄 エラーハンドリングと回復

プロダクション環境では、エラーハンドリングの挙動が重要です。

async は伝統的に Node.js のエラーファーストコールバックパターンに従いますが、Promise ベースの API も提供しています。エラー発生時の挙動は関数によって異なります。

// async: エラーハンドリング
import { map } from 'async';

try {
  const results = await map(files, async (file) => {
    if (file === 'bad.txt') throw new Error('Failed');
    return await readFile(file);
  });
} catch (error) {
  // 最初のエラーで停止
  console.error(error);
}

bottleneck はエラー発生後もキューを継続でき、再試行ロジックを組み込むことができます。

// bottleneck: エラー後の継続
limiter.schedule(async () => {
  try {
    return await api.call();
  } catch (error) {
    // エラーを握りつぶして継続
    return null;
  }
});

p-queue は Promise の標準的なエラーハンドリングに従い、個別のタスクエラーがキュー全体を停止させることはありません。

// p-queue: 個別タスクのエラー処理
queue.add(async () => {
  try {
    return await fetch('/api/data');
  } catch (error) {
    console.error('Task failed:', error);
    return null; // キューは継続
  }
});

promise-queuequeue-promise も同様に Promise の標準挙動に従いますが、エラー回復の機能は限定的です。

// promise-queue: エラー処理
queue.add(() => {
  return fetch('/api/data').catch(err => {
    console.error(err);
    return null;
  });
});

📦 実装サイズと依存関係

バンドルサイズを気にするフロントエンドプロジェクトでは、依存関係の少なさが重要です。

  • async: 依存関係なし、ただしコードベースが大きい
  • bottleneck: 依存関係なし、中程度のサイズ
  • p-queue: 依存関係あり(eventemitter3)、軽量
  • promise-queue: 依存関係なし、非常に軽量
  • queue-promise: 依存関係なし、軽量
// インポート例の比較
import { map } from 'async';           // 全体または個別
import Bottleneck from 'bottleneck';   // デフォルトエクスポート
import PQueue from 'p-queue';          // デフォルトエクスポート
import PromiseQueue from 'promise-queue'; // デフォルトエクスポート
import Queue from 'queue-promise';     // デフォルトエクスポート

🌐 ブラウザと Node.js の互換性

すべてのパッケージがブラウザと Node.js の両方で動作しますが、注意点があります。

async は最も広い互換性を持ち、古いブラウザでも動作しますが、ポリフィルが必要な場合があります。

bottleneck は Redis アダプターを使用する場合、Node.js 環境でのみ完全な機能を利用できます。ブラウザではローカルモードになります。

// bottleneck: Redis 連携(Node.js のみ)
const limiter = new Bottleneck({
  datastore: 'redis',
  redisClient: new Redis()
});

p-queue はブラウザと Node.js の両方で完全に動作し、モダンな JavaScript 機能を使用しています。

promise-queuequeue-promise も両環境で動作しますが、機能セットが限定的です。

🛠️ 保守状況とコミュニティ

パッケージの長期的な使用を考えると、メンテナンス状況が重要です。

  • async: 非常に活発にメンテナンスされており、事実上の標準ライブラリ
  • bottleneck: 活発にメンテナンスされており、企業でも使用されている
  • p-queue: 活発にメンテナンスされており、Sindre Sorhus 氏のエコシステムの一部
  • promise-queue: メンテナンス頻度は低く、新機能追加は少ない
  • queue-promise: メンテナンス状況を確認が必要、機能追加は限定的

💡 実戦での選択ガイド

シナリオ 1: API レート制限の厳守

外部 API を呼び出す際に、レート制限(例:1 分間に 100 リクエスト)を厳守する必要がある場合:

推奨: bottleneck

const limiter = new Bottleneck({
  minTime: 600,  // 600ms ごとに 1 リクエスト
  maxConcurrent: 1
});

const results = await Promise.all(
  urls.map(url => limiter.schedule(() => fetch(url)))
);

シナリオ 2: 画像の並列アップロード

複数の画像を並列でアップロードしたいが、ブラウザを重くしたくない場合:

推奨: p-queue

const queue = new PQueue({ concurrency: 5 });

const uploadJobs = images.map(img => 
  queue.add(() => uploadImage(img))
);

await Promise.all(uploadJobs);

シナリオ 3: レガシーコードの維持

既存のコードベースで async が既に使用されており、新しい機能追加が必要な場合:

推奨: async(一貫性の維持)

import { series } from 'async';

await series([
  (cb) => migrateDatabase(cb),
  (cb) => clearCache(cb),
  (cb) => restartService(cb)
]);

シナリオ 4: シンプルな FIFO キュー

非常にシンプルなキュー機能のみが必要で、依存関係を増やしたくない場合:

推奨: promise-queue または queue-promise

const queue = new PromiseQueue(1, Infinity);

queue.add(() => processItem(item1));
queue.add(() => processItem(item2));

📊 総合比較表

機能asyncbottleneckp-queuepromise-queuequeue-promise
並行制御限定的✅ 高度✅ 簡易✅ 簡易
レート制限✅ 高度⚠️ 簡易
優先度キュー
分散対応✅ (Redis)
Promise 対応
保守状況✅ 活発✅ 活発✅ 活発⚠️ 低⚠️ 要確認
バンドルサイズ最小

🎯 結論:どのパッケージを選ぶべきか

新規プロジェクトでは、p-queue または bottleneck の使用を推奨します。これらはモダンな Promise ベースの API を提供し、活発にメンテナンスされています。

  • レート制限が必要bottleneck
  • シンプルな並行制御p-queue
  • コレクション操作も必要async
  • 最小限の依存関係promise-queue

レガシープロジェクトでは、既存のコードベースとの一貫性を保つために、既に使用されているライブラリを継続使用することが賢明です。

最終的に、プロジェクトの要件(レート制限、並行制御、保守性)とチームの習熟度を考慮して選択してください。どのライブラリも適切に使用すれば、非同期処理の制御を大幅に改善できます。

選び方: async vs queue-promise vs bottleneck vs p-queue vs promise-queue

  • async:

    async は既に大規模なコードベースで使用されている場合や、コレクション操作(map、filter、reduce など)の非同期バージョンを包括的に必要とする場合に選択します。ただし、新規プロジェクトではモダンな Promise ベースの代替案を検討すべきです。

  • queue-promise:

    基本的な Promise キュー機能が必要ですが、p-queue の機能が多すぎる場合に検討できます。ただし、メンテナンス状況をよく確認してから使用してください。

  • bottleneck:

    API レート制限を厳密に遵守する必要がある場合や、複雑な負荷分散ロジックが必要な場合に bottleneck を選択します。Redis 連携による分散環境でのレート制限にも対応しています。

  • p-queue:

    シンプルで軽量な Promise キューが必要な場合、または並行処理数を制限したい場合に p-queue を選択します。モダンなコードベースとの親和性が高く、メンテナンスも活発です。

  • promise-queue:

    非常にシンプルな FIFO キュー機能のみが必要で、追加の機能や活発なメンテナンスを必要としないレガシープロジェクトでのみ検討します。新規プロジェクトでは非推奨です。

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)
})