1. 异步编程中的await陷阱:从5秒到0.5秒的性能跃迁
在Node.js开发中,异步操作的处理方式直接影响着应用性能。我曾接手过一个用户列表页加载需要6秒多的项目,排查后发现数据库查询仅耗时200ms,而剩余5秒多都消耗在了一个看似无害的for...of循环里——开发者在这里使用了串行await,导致本可并行的10个请求变成了排队执行。这种性能损耗在大型应用中会被放大数十倍,而解决方案往往只需要调整几行代码。
1.1 循环中的串行await:性能黑洞
最常见的性能陷阱就是在循环中不加区分地使用await。假设我们需要获取多个用户资料,新手常会写出这样的代码:
javascript复制async function getUserProfiles(userIds) {
const profiles = [];
for (const id of userIds) {
const profile = await fetchProfile(id);
profiles.push(profile);
}
return profiles;
}
这段代码的问题在于:每个await都会阻塞循环,导致请求被串行执行。如果单个请求耗时500ms,10个请求就需要5000ms(5秒)。实际上这些请求之间没有依赖关系,完全应该并行执行。
优化方案是使用Promise.all:
javascript复制async function getUserProfiles(userIds) {
const promises = userIds.map(id => fetchProfile(id));
const profiles = await Promise.all(promises);
return profiles;
}
改造后,10个请求同时发出,总耗时约等于最慢的那个请求的时间(500-600ms),性能提升近10倍。这个案例告诉我们:在循环中使用await前,务必确认每次迭代是否真的依赖前一次的结果。
提示:当使用
Array.map生成Promise数组时,注意回调函数必须是同步的。如果需要在map中进行异步操作,应该改用for...of循环配合Promise.all。
1.2 Promise.all的"全有或全无"特性
虽然Promise.all能显著提升性能,但它有个重要特性:只要有一个Promise被reject,整个Promise.all就会立即reject,其他成功的Promise结果也会被丢弃。这在需要容忍部分失败的场景下就不适用了。
javascript复制async function getUserProfiles(userIds) {
try {
const profiles = await Promise.all(
userIds.map(id => fetchProfile(id))
);
return profiles;
} catch (error) {
// 即使有部分成功,这里也拿不到它们的结果
return [];
}
}
对于需要获取所有结果(无论成功失败)的场景,应该使用ES2020引入的Promise.allSettled:
javascript复制async function getUserProfiles(userIds) {
const results = await Promise.allSettled(
userIds.map(id => fetchProfile(id))
);
const profiles = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failures = results
.filter(r => r.status === 'rejected');
if (failures.length > 0) {
