Event loop 在单个线程上运行你的 JavaScript,因此任何耗时较长的同步操作都会阻塞它——而在阻塞期间,Node 不会服务任何其他 request、timer 或 callback。阻塞几乎总是CPU-bound 工作或某个 *Sync API,绝不会是被正确 await 的异步 I/O。
*Sync)API — fs.readFileSync、fs.writeFileSync、child_process.execSync、zlib.gzipSync。它们就地等待,而不是交给 libuv。map/sort/filter、矩阵运算、图像/视频处理、对巨大结构的 parsing/serializing。JSON.parse / JSON.stringify — 同步且 O(n);一个数 MB 的 payload 会在整个 parse 期间冻结 loop。crypto.pbkdf2Sync、bcrypt 同步模式、在主线程上对大 buffer 进行 hashing。while (true)、无界递归、对数百万个 item 且每个 item 都有工作的 for。// 以下这些都会阻塞 loop——在此期间不会服务任何其他 request:
const data = fs.readFileSync("big.json"); // 同步 I/O
const obj = JSON.parse(hugeString); // 大型 parse
const hash = crypto.pbkdf2Sync(pw, salt, 1e6, 64, "sha512"); // 同步 crypto
for (let i = 0; i < 1e9; i++) { /* CPU */ } // 紧密循环
把 CPU 工作包进一个
async函数无济于事——async只在await处让出;同步的函数体仍会在 loop 线程上一直运行到结束。
perf_hooks.monitorEventLoopDelay() 或 blocked-at 之类的库来测量;上升的 lag 意味着有东西在霸占线程。node --prof、clinic doctor/clinic flame、Chrome DevTools CPU profile,用来找出热点同步栈帧。*Sync 调用使用其异步对应版本(fs.promises.readFile、crypto.pbkdf2、zlib.gzip)。worker_threads 池、child process 或外部 queue/service。setImmediate/await 将长循环分块(chunk),让 loop 在各批次之间得以喘息。const { Worker } = require("node:worker_threads");
// 把昂贵的 hash 卸载出去,让 loop 保持响应。
const worker = new Worker("./hash-worker.js", { workerData: { pw, salt } });
在 Node 中,一次阻塞调用会惩罚每一个并发客户端,而不仅仅是引发它的那个 request——延迟飙升、health check 超时、进程在负载下看起来像"卡死"了。面试官问这个是想看你能否说出具体的阻塞源(*Sync、大型 JSON.parse、同步 crypto、ReDoS、CPU 循环),用 event-loop lag 工具检测它们,并通过转为异步或卸载到 worker threads 来修复它们。这是 Node 在生产环境中最常见的单一性能失误。
一个包含详细解答的 IT 面试题库——从初级到高级。
捐赠