这三个库都用于控制代码执行的频率和并发量,但核心机制不同。p-limit 用于限制同时运行的任务数量(并发度);p-throttle 用于限制单位时间内的调用次数(速率);limiter 提供基于令牌桶算法的底层限流,更偏向 Node.js 服务端场景。前端开发中通常首选 p-* 系列以获得更好的 Promise 支持。
在前端和 Node.js 开发中,控制任务执行的速度和数量是常见需求。limiter、p-limit 和 p-throttle 都解决了这个问题,但它们的适用场景和实现方式有很大区别。让我们从实际工程角度看看它们如何工作。
p-limit 关注的是同时有多少个任务在运行。
// 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 关注的是单位时间内允许通过多少个任务。
// 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 是选择的重要标准。
p-limit 完全基于 Promise 设计。
// p-limit: 原生 Promise 支持
async function run() {
const limit = pLimit(2);
await limit(() => doSomething());
}
p-throttle 同样完全基于 Promise 设计。
// p-throttle: 原生 Promise 支持
async function run() {
const throttle = pThrottle({limit: 1, interval: 1000});
try {
await throttle(() => doSomething());
} catch (e) {
// 直接捕获错误
}
}
limiter 主要基于回调函数(Callback)。
// 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) => {
// 如果没有令牌,回调会阻塞直到可用
// 需要自己处理超时或放弃逻辑
});
你需要上传 50 张图片,但浏览器同时连接数有限,且不想卡死界面。
p-limit// 使用 p-limit
const limit = pLimit(5);
const uploads = images.map(img => limit(() => upload(img)));
await Promise.all(uploads);
第三方 API 规定每秒最多调用 10 次,否则会返回 429 错误。
p-throttle// 使用 p-throttle
const throttle = pThrottle({limit: 10, interval: 1000});
const callApi = throttle(() => fetch('/external-api'));
await callApi();
你在编写一个 API 网关,需要根据用户等级分配不同的令牌速率。
limiter// 使用 limiter
const userLimiter = new RateLimiter(userTokens, 'second');
userLimiter.removeTokens(1, callback);
| 特性 | p-limit | p-throttle | limiter |
|---|---|---|---|
| 控制目标 | 同时运行的任务数 | 单位时间内的调用次数 | 令牌消耗速率 |
| 异步风格 | Promise / async-await | Promise / async-await | 回调函数 (Callback) |
| 主要场景 | 并发控制 (上传/下载) | 速率限制 (API 调用) | 服务端流控 (网关) |
| 前端友好度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 配置复杂度 | 低 (仅并发数) | 中 (次数 + 时间) | 高 (令牌 + 间隔) |
对于现代前端开发,优先选择 p-limit 或 p-throttle。
p-limit。p-throttle。limiter,因为它的回调风格会增加代码维护成本,且不如 p-* 系列对 Promise 友好。这三个工具都很小巧,但它们解决的是不同维度的问题。选对工具能让你的异步代码更稳定,也更易读。
选择 limiter 如果你在 Node.js 服务端需要实现复杂的令牌桶算法,或者需要精细控制令牌生成速率。由于它主要基于回调函数,不太适合现代前端基于 async/await 的流程,新项目建议优先考虑其他方案。
选择 p-limit 如果你需要限制同时执行的任务数量,例如同时只允许 5 个文件上传或 3 个 API 请求。它非常适合控制并发度,防止浏览器或服务器因过多同时请求而过载。
选择 p-throttle 如果你需要限制单位时间内的调用频率,例如每秒最多调用 10 次 API。它适合应对有严格速率限制的第三方接口,确保不会因请求过快而被封禁。
Provides a generic rate limiter for the web and node.js. Useful for API clients, web crawling, or other tasks that need to be throttled. Two classes are exposed, RateLimiter and TokenBucket. TokenBucket provides a lower level interface to rate limiting with a configurable burst rate and drip rate. RateLimiter sits on top of the token bucket and adds a restriction on the maximum number of tokens that can be removed each interval to comply with common API restrictions such as "150 requests per hour maximum".
yarn add limiter
A simple example allowing 150 requests per hour:
import { RateLimiter } from "limiter";
// Allow 150 requests per hour (the Twitter search limit). Also understands
// 'second', 'minute', 'day', or a number of milliseconds
const limiter = new RateLimiter({ tokensPerInterval: 150, interval: "hour" });
async function sendRequest() {
// This call will throw if we request more than the maximum number of requests
// that were set in the constructor
// remainingRequests tells us how many additional requests could be sent
// right this moment
const remainingRequests = await limiter.removeTokens(1);
callMyRequestSendingFunction(...);
}
Another example allowing one message to be sent every 250ms:
import { RateLimiter } from "limiter";
const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 250 });
async function sendMessage() {
const remainingMessages = await limiter.removeTokens(1);
callMyMessageSendingFunction(...);
}
The default behaviour is to wait for the duration of the rate limiting that's
currently in effect before the promise is resolved, but if you pass in
"fireImmediately": true, the promise will be resolved immediately with
remainingRequests set to -1:
import { RateLimiter } from "limiter";
const limiter = new RateLimiter({
tokensPerInterval: 150,
interval: "hour",
fireImmediately: true
});
async function requestHandler(request, response) {
// Immediately send 429 header to client when rate limiting is in effect
const remainingRequests = await limiter.removeTokens(1);
if (remainingRequests < 0) {
response.writeHead(429, {'Content-Type': 'text/plain;charset=UTF-8'});
response.end('429 Too Many Requests - your IP is being rate limited');
} else {
callMyMessageSendingFunction(...);
}
}
A synchronous method, tryRemoveTokens(), is available in both RateLimiter and TokenBucket. This will return immediately with a boolean value indicating if the token removal was successful.
import { RateLimiter } from "limiter";
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });
if (limiter.tryRemoveTokens(5))
console.log('Tokens removed');
else
console.log('No tokens removed');
To get the number of remaining tokens outside the removeTokens promise,
simply use the getTokensRemaining method.
import { RateLimiter } from "limiter";
const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 250 });
// Prints 1 since we did not remove a token and our number of tokens per
// interval is 1
console.log(limiter.getTokensRemaining());
Using the token bucket directly to throttle at the byte level:
import { TokenBucket } from "limiter";
const BURST_RATE = 1024 * 1024 * 150; // 150KB/sec burst rate
const FILL_RATE = 1024 * 1024 * 50; // 50KB/sec sustained rate
// We could also pass a parent token bucket in to create a hierarchical token
// bucket
// bucketSize, tokensPerInterval, interval
const bucket = new TokenBucket({
bucketSize: BURST_RATE,
tokensPerInterval: FILL_RATE,
interval: "second"
});
async function handleData(myData) {
await bucket.removeTokens(myData.byteLength);
sendMyData(myData);
}
Both the token bucket and rate limiter should be used with a message queue or some way of preventing multiple simultaneous calls to removeTokens(). Otherwise, earlier messages may get held up for long periods of time if more recent messages are continually draining the token bucket. This can lead to out of order messages or the appearance of "lost" messages under heavy load.
MIT License