p-limit、p-queue 和 p-throttle 都是用于管理 JavaScript 异步操作执行流程的工具库,旨在解决并发控制、任务排队和频率限制等常见问题。p-limit 专注于限制同时运行的 Promise 数量,防止资源耗尽;p-queue 提供了一个功能更丰富的优先级队列,支持并发限制、超时管理和空闲监听;p-throttle 则专注于基于时间间隔的频率限制,确保函数调用不会超过设定的速率。这三个库通常配合使用,以构建稳健的异步数据抓取、API 调用或批量处理系统。
在构建高性能的前端或 Node.js 应用时,管理异步操作的并发量至关重要。无节制的并发可能导致浏览器卡顿、服务器过载或触发 API 速率限制。p-limit、p-queue 和 p-throttle 是 JavaScript 生态中解决这些问题的三个核心工具。虽然它们都涉及“控制执行”,但侧重点完全不同。让我们深入对比它们的工作原理和适用场景。
这三个库解决了异步流程中三个不同维度的问题。
p-limit 关注的是**“同时能跑几个”**。
它像一个并发控制器,确保同一时刻只有固定数量的 Promise 在运行。多余的请求会排队等待,直到有名额空出。
// p-limit: 限制并发数为 2
import pLimit from 'p-limit';
const limit = pLimit(2);
const input = [1, 2, 3, 4, 5];
// 同时最多只有 2 个 fetcher 在运行
const result = await Promise.all(input.map(id => limit(() => fetcher(id))));
p-queue 关注的是**“任务怎么排队和执行”**。
它是一个完整的任务队列,不仅限制并发,还支持优先级、超时、以及监听队列清空事件。
// p-queue: 带优先级的队列
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 2 });
// 普通任务
queue.add(() => fetcher('normal'));
// 高优先级任务(插队)
queue.add(() => fetcher('urgent'), { priority: 10 });
// 等待所有任务完成
await queue.onIdle();
p-throttle 关注的是**“多久能跑一次”**。
它基于时间窗口进行限流,确保函数在特定时间间隔内只执行固定次数,不管并发如何。
// p-throttle: 限制每秒最多 1 次
import pThrottle from 'p-throttle';
const throttle = pThrottle({
limit: 1,
interval: 1000
});
const throttledFetcher = throttle(id => fetcher(id));
// 即使瞬间调用 10 次,也会按时间间隔依次执行
input.forEach(id => throttledFetcher(id));
API 的设计反映了它们的使用模式。
p-limit 返回一个包装函数。
你需要用这个限制函数包裹你的异步任务生成器。它非常适合嵌入到现有的 Promise.all 或 map 操作中。
// p-limit: 包装器模式
const limit = pLimit(5);
const tasks = urls.map(url => limit(() => download(url)));
await Promise.all(tasks);
p-queue 实例化一个队列对象。
你将任务“添加”到队列中,队列负责调度。这更适合长运行的后台任务或需要动态添加任务的场景。
// p-queue: 实例模式
const queue = new PQueue({ concurrency: 5 });
urls.forEach(url => queue.add(() => download(url)));
await queue.onIdle();
p-throttle 返回一个限流函数。
类似于 p-limit,但它控制的是调用频率。你直接调用返回的函数,它会自动处理延迟。
// p-throttle: 限流函数模式
const throttledSave = pThrottle({ limit: 1, interval: 2000 })(saveToApi);
// 调用后,如果超限,返回的 Promise 会延迟 resolve
await throttledSave(data);
在处理混合重要性的任务时,调度策略至关重要。
p-limit 不支持优先级。
任务严格按照先进先出(FIFO)的顺序等待执行。你无法让一个紧急任务插队。
// p-limit: 无优先级支持
// 所有任务平等排队,无法干预顺序
const limit = pLimit(1);
limit(() => console.log('Task 1'));
limit(() => console.log('Task 2'));
// 无法让 Task 2 先执行
p-queue 原生支持优先级。
你可以为每个任务分配优先级数值,高优先级的任务会优先获取执行名额。这对于处理用户交互触发的任务非常有用。
// p-queue: 支持优先级
const queue = new PQueue({ concurrency: 1 });
queue.add(() => console.log('Low'), { priority: 1 });
queue.add(() => console.log('High'), { priority: 10 });
// 'High' 会先于 'Low' 执行
p-throttle 不支持优先级。
它的核心是时间控制,调用顺序决定了执行顺序。它不管理任务队列的排序,只管理调用的时间间隔。
// p-throttle: 无优先级支持
// 严格按照调用顺序和时间间隔执行
const fn = pThrottle({ limit: 1, interval: 1000 })(task);
fn(); fn(); fn();
// 按调用顺序依次延迟执行
在批量处理中,如何处理错误和监控进度是工程化的关键。
p-limit 将错误抛回给调用者。
它本身不捕获错误,也不提供队列状态查询。如果某个任务失败,Promise.all 会立即拒绝(除非你自己在任务内部 catch)。
// p-limit: 错误透传
try {
await Promise.all([limit(() => success()), limit(() => fail())]);
} catch (error) {
// 任何一个失败都会捕获到这里
}
p-queue 提供丰富的状态监控。
你可以监听 error 事件,查询 size(排队数)和 pending(运行数)。它允许队列在任务失败后继续运行其他任务(取决于配置)。
// p-queue: 状态监控
const queue = new PQueue();
queue.on('error', err => console.error(err));
console.log(queue.size); // 排队数量
console.log(queue.pending); // 运行中数量
p-throttle 专注于调用控制。
它不管理任务队列的状态,只保证函数调用的频率。错误处理完全由被包装的函数决定。
// p-throttle: 错误由原函数处理
const fn = pThrottle({ limit: 1, interval: 1000 })(async () => {
const res = await api.call();
if (!res.ok) throw new Error('Failed');
return res;
});
// 错误会作为 Promise rejection 返回
你需要加载 100 张图片,但浏览器同时打开太多连接会阻塞。
p-limitconst limit = pLimit(6);
const images = urls.map(url => limit(() => loadImage(url)));
await Promise.all(images);
一个 Node.js 服务需要处理用户上传的任务,VIP 用户的任务需要优先处理。
p-queueconst queue = new PQueue({ concurrency: 5 });
tasks.forEach(task => {
const priority = task.user.isVip ? 10 : 1;
queue.add(() => process(task), { priority });
});
await queue.onIdle();
process.exit(0);
某 API 限制每分钟只能调用 60 次,否则会封禁。
p-throttleconst callApi = pThrottle({ limit: 60, interval: 60000 })(fetchData);
dataIds.forEach(id => callApi(id));
| 特性 | p-limit | p-queue | p-throttle |
|---|---|---|---|
| 核心目标 | 限制并发数量 | 任务队列管理 | 限制调用频率 |
| 并发控制 | ✅ 支持 | ✅ 支持 | ❌ 不直接支持 |
| 时间限流 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 (Interval) |
| 优先级 | ❌ 不支持 | ✅ 支持 | ❌ 不支持 |
| 空闲监听 | ❌ 不支持 | ✅ onIdle() | ❌ 不支持 |
| API 形态 | 包装函数 | 类实例 | 包装函数 |
| 适用复杂度 | 低 | 高 | 中 |
尽管功能不同,这三个库在设计哲学上有很多相似之处,这使得它们可以很好地协同工作。
async/await。// 三者都返回 Promise
await limit(() => task());
await queue.add(() => task());
await throttledFn();
// 都可以用于浏览器环境
// 不会像某些重型库那样增加显著的打包体积
p-throttle 限制频率,内部再用 p-limit 限制并发。// 组合示例:每秒最多 10 次,每次并发 2 个
const throttle = pThrottle({ limit: 10, interval: 1000 });
const limit = pLimit(2);
const run = throttle(() => limit(() => task()));
p-limit 是日常开发中最常用的工具 🛠️。如果你只是觉得 Promise.all 跑得太快,导致内存爆炸或 API 报错,先加一层 p-limit 通常就能解决问题。它简单、直观、无脑接入。
p-queue 是构建健壮后台服务的基石 🏗️。当你需要构建一个任务处理器、爬虫或需要保证任务顺序和优先级的系统时,它的队列管理能力是不可或缺的。特别是 onIdle 方法,对于编写自动化脚本非常有用。
p-throttle 是合规与保护的盾牌 🛡️。当面对外部约束(如 API Rate Limit)或需要平滑用户操作(如防止按钮狂点)时,它是最佳选择。注意,它和 p-limit 不同,它不控制“同时”,而是控制“频率”。
最终建议:在大多数前端数据获取场景中,p-limit 足以应付 90% 的需求。只有当你明确需要优先级调度或严格的时间限流时,才引入 p-queue 或 p-throttle。保持依赖精简,选择最匹配问题的工具。
如果你只需要简单地限制同时运行的异步任务数量(例如限制同时发起 5 个网络请求),选择 p-limit。它是最轻量级的方案,API 极其简单,适合不需要排队、优先级或复杂状态管理的场景。
如果你需要更复杂的任务调度,例如支持任务优先级、需要等待所有任务完成(onIdle)、或者需要暂停/恢复队列,选择 p-queue。它适合构建需要精细控制的后台处理系统或复杂的批量作业流程。
如果你的主要需求是限制单位时间内的执行次数(例如 API 限制每秒最多调用 10 次),选择 p-throttle。它专注于时间维度的限流,适合对接有严格速率限制的第三方服务或防止 UI 事件触发过于频繁。
Run multiple promise-returning & async functions with limited concurrency
Works in Node.js and browsers.
npm install p-limit
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);
Returns a limit function.
Type: number | object
Minimum: 1
Concurrency limit.
You can pass a number or an options object with a concurrency property.
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});
Returns the promise returned by calling fn(...args).
Type: Function
Promise-returning/async function.
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.
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.
The number of promises that are currently running.
The number of promises that are waiting to run (i.e. their internal fn was not called yet).
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().
Get or set the concurrency limit.
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);
Type: Function
Promise-returning/async function.
Type: object
Type: number
Minimum: 1
Concurrency limit.
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().
See recipes.md for common use cases and patterns.
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.