p-limit vs limiter vs p-throttle
JavaScript 任务并发与速率限制方案对比
p-limitlimiterp-throttle类似的npm包:

JavaScript 任务并发与速率限制方案对比

这三个库都用于控制代码执行的频率和并发量,但核心机制不同。p-limit 用于限制同时运行的任务数量(并发度);p-throttle 用于限制单位时间内的调用次数(速率);limiter 提供基于令牌桶算法的底层限流,更偏向 Node.js 服务端场景。前端开发中通常首选 p-* 系列以获得更好的 Promise 支持。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
p-limit101,915,9922,82814.9 kB12 个月前MIT
limiter01,558158 kB141 年前MIT
p-throttle051521.6 kB05 个月前MIT

JavaScript 任务并发与速率限制方案对比

在前端和 Node.js 开发中,控制任务执行的速度和数量是常见需求。limiterp-limitp-throttle 都解决了这个问题,但它们的适用场景和实现方式有很大区别。让我们从实际工程角度看看它们如何工作。

🚀 核心机制:并发数 vs 速率 vs 令牌桶

p-limit 关注的是同时有多少个任务在运行

  • 它限制的是并发度(Concurrency)。
  • 即使你有 100 个任务,如果限制为 2,那么同一时刻只有 2 个在执行,其余排队。
// p-limit: 限制同时运行 2 个任务
import pLimit from 'p-limit';
const limit = pLimit(2);

const jobs = Array.from({ length: 5 }).map(() => 
  limit(() => fetch('/api/data'))
);
await Promise.all(jobs);
// 同一时刻最多 2 个 fetch 在进行

p-throttle 关注的是单位时间内允许通过多少个任务

  • 它限制的是速率(Rate)。
  • 即使并发只有 1 个,如果限制每秒 2 次,第 3 次调用会被延迟到下一秒。
// p-throttle: 限制每秒最多 2 次调用
import pThrottle from 'p-throttle';
const throttle = pThrottle({limit: 2, interval: 1000});

const throttledFetch = throttle(() => fetch('/api/data'));
await throttledFetch();
await throttledFetch();
await throttledFetch(); // 这次会被延迟到下一个时间窗口

limiter 关注的是令牌桶算法

  • 它维护一个令牌池,每次执行消耗令牌,令牌随时间自动补充。
  • 更灵活,但配置更复杂,通常用于服务端限流。
// limiter: 每秒生成 2 个令牌,桶容量 2
const { RateLimiter } = require('limiter');
const limiter = new RateLimiter(2, 'second');

limiter.removeTokens(1, (err, remaining) => {
  // 拿到令牌后才能执行请求
  fetch('/api/data');
});

📦 Promise 支持:现代异步流程的关键

对于前端开发者来说,是否原生支持 Promise 是选择的重要标准。

p-limit 完全基于 Promise 设计。

  • 返回的是一个 Promise。
  • 可以直接配合 async/await 使用,代码清晰。
// p-limit: 原生 Promise 支持
async function run() {
  const limit = pLimit(2);
  await limit(() => doSomething());
}

p-throttle 同样完全基于 Promise 设计。

  • 包装后的函数返回 Promise。
  • 错误处理可以直接用 try/catch。
// p-throttle: 原生 Promise 支持
async function run() {
  const throttle = pThrottle({limit: 1, interval: 1000});
  try {
    await throttle(() => doSomething());
  } catch (e) {
    // 直接捕获错误
  }
}

limiter 主要基于回调函数(Callback)。

  • 默认 API 使用回调,不支持直接 await。
  • 在现代 async/await 代码中需要额外包装,增加复杂度。
// limiter: 主要基于回调
function run() {
  limiter.removeTokens(1, (err, remaining) => {
    // 需要在回调里继续逻辑
    // 如果要转 Promise 需手动封装
  });
}

⚠️ 错误处理与排队行为

当任务失败或队列满时,各包的表现不同。

p-limit 会保留所有任务直到执行。

  • 任务会排队等待,不会因为并发满而丢弃。
  • 如果某个任务报错,不影响其他任务。
// p-limit: 任务排队,错误隔离
const limit = pLimit(2);
// 即使并发满,新任务也会进入队列等待
await limit(() => riskyOperation()); 

p-throttle 会延迟执行直到时间窗口允许。

  • 任务不会丢失,只是被推迟。
  • 适合需要保证最终执行但不在乎稍慢的场景。
// p-throttle: 自动延迟
const throttle = pThrottle({limit: 1, interval: 1000});
// 调用过快会自动排队延迟执行
await throttle(() => apiCall()); 

limiter 取决于你如何处理回调。

  • 如果没有令牌,回调会等待直到有令牌。
  • 需要手动处理等待逻辑,不如 p-* 系列自动化。
// limiter: 手动处理等待
limiter.removeTokens(1, (err, remaining) => {
  // 如果没有令牌,回调会阻塞直到可用
  // 需要自己处理超时或放弃逻辑
});

🌐 真实场景推荐

场景 1:批量上传图片

你需要上传 50 张图片,但浏览器同时连接数有限,且不想卡死界面。

  • 最佳选择p-limit
  • 理由:限制同时上传数量(如 5 个),避免占用所有网络资源。
// 使用 p-limit
const limit = pLimit(5);
const uploads = images.map(img => limit(() => upload(img)));
await Promise.all(uploads);

场景 2:调用有限制的 API

第三方 API 规定每秒最多调用 10 次,否则会返回 429 错误。

  • 最佳选择p-throttle
  • 理由:严格控制时间窗口内的请求次数,避免触发限流。
// 使用 p-throttle
const throttle = pThrottle({limit: 10, interval: 1000});
const callApi = throttle(() => fetch('/external-api'));
await callApi();

场景 3:Node.js 服务端网关

你在编写一个 API 网关,需要根据用户等级分配不同的令牌速率。

  • 最佳选择limiter
  • 理由:令牌桶算法更适合复杂的服务端流控策略。
// 使用 limiter
const userLimiter = new RateLimiter(userTokens, 'second');
userLimiter.removeTokens(1, callback);

📊 核心差异总结

特性p-limitp-throttlelimiter
控制目标同时运行的任务数单位时间内的调用次数令牌消耗速率
异步风格Promise / async-awaitPromise / async-await回调函数 (Callback)
主要场景并发控制 (上传/下载)速率限制 (API 调用)服务端流控 (网关)
前端友好度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
配置复杂度低 (仅并发数)中 (次数 + 时间)高 (令牌 + 间隔)

💡 最终建议

对于现代前端开发,优先选择 p-limitp-throttle

  • 如果你的问题是**“太多任务同时跑会卡死”**,用 p-limit
  • 如果你的问题是**“请求太快会被服务器拒绝”**,用 p-throttle
  • 除非你在 Node.js 服务端需要复杂的令牌桶逻辑,否则不建议在前端使用 limiter,因为它的回调风格会增加代码维护成本,且不如 p-* 系列对 Promise 友好。

这三个工具都很小巧,但它们解决的是不同维度的问题。选对工具能让你的异步代码更稳定,也更易读。

如何选择: p-limit vs limiter vs p-throttle

  • p-limit:

    选择 p-limit 如果你需要限制同时执行的任务数量,例如同时只允许 5 个文件上传或 3 个 API 请求。它非常适合控制并发度,防止浏览器或服务器因过多同时请求而过载。

  • limiter:

    选择 limiter 如果你在 Node.js 服务端需要实现复杂的令牌桶算法,或者需要精细控制令牌生成速率。由于它主要基于回调函数,不太适合现代前端基于 async/await 的流程,新项目建议优先考虑其他方案。

  • p-throttle:

    选择 p-throttle 如果你需要限制单位时间内的调用频率,例如每秒最多调用 10 次 API。它适合应对有严格速率限制的第三方接口,确保不会因请求过快而被封禁。

p-limit的README

p-limit

Run multiple promise-returning & async functions with limited concurrency

Works in Node.js and browsers.

Install

npm install p-limit

Usage

import pLimit from 'p-limit';

const limit = pLimit(1);

const input = [
	limit(() => fetchSomething('foo')),
	limit(() => fetchSomething('bar')),
	limit(() => doSomething())
];

// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);

API

pLimit(concurrency) default export

Returns a limit function.

concurrency

Type: number | object
Minimum: 1

Concurrency limit.

You can pass a number or an options object with a concurrency property.

rejectOnClear

Type: boolean
Default: false

Reject pending promises with an AbortError when clearQueue() is called. This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().

import pLimit from 'p-limit';

const limit = pLimit({concurrency: 1});

limit(fn, ...args)

Returns the promise returned by calling fn(...args).

fn

Type: Function

Promise-returning/async function.

args

Any arguments to pass through to fn.

Support for passing arguments on to the fn is provided in order to be able to avoid creating unnecessary closures. You probably don't need this optimization unless you're pushing a lot of functions.

Warning: Avoid calling the same limit function inside a function that is already limited by it. This can create a deadlock where inner tasks never run. Use a separate limiter for inner tasks.

limit.map(iterable, mapperFunction)

Process an iterable of inputs with limited concurrency.

The mapper function receives the item value and its index.

Returns a promise equivalent to Promise.all(Array.from(iterable, (item, index) => limit(mapperFunction, item, index))).

This is a convenience function for processing inputs that arrive in batches. For more complex use cases, see p-map.

limit.activeCount

The number of promises that are currently running.

limit.pendingCount

The number of promises that are waiting to run (i.e. their internal fn was not called yet).

limit.clearQueue()

Discard pending promises that are waiting to run.

This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.

Note: This does not cancel promises that are already running.

When rejectOnClear is enabled, pending promises are rejected with an AbortError. This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().

limit.concurrency

Get or set the concurrency limit.

limitFunction(fn, options) named export

Returns a function with limited concurrency.

The returned function manages its own concurrent executions, allowing you to call it multiple times without exceeding the specified concurrency limit.

Ideal for scenarios where you need to control the number of simultaneous executions of a single function, rather than managing concurrency across multiple functions.

import {limitFunction} from 'p-limit';

const limitedFunction = limitFunction(async () => {
	return doSomething();
}, {concurrency: 1});

const input = Array.from({length: 10}, limitedFunction);

// Only one promise is run at once.
await Promise.all(input);

fn

Type: Function

Promise-returning/async function.

options

Type: object

concurrency

Type: number
Minimum: 1

Concurrency limit.

rejectOnClear

Type: boolean
Default: false

Reject pending promises with an AbortError when clearQueue() is called. This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().

Recipes

See recipes.md for common use cases and patterns.

FAQ

How is this different from the p-queue package?

This package is only about limiting the number of concurrent executions, while p-queue is a fully featured queue implementation with lots of different options, introspection, and ability to pause the queue.

Related

  • p-throttle - Throttle promise-returning & async functions
  • p-debounce - Debounce promise-returning & async functions
  • p-map - Run promise-returning & async functions concurrently with different inputs
  • p-all - Run promise-returning & async functions concurrently with optional limited concurrency
  • More…