← 中文资源

简体中文版本

事件循环饥饿

长时间同步工作或无界微任务会阻止定时器、I/O 与渲染运行,使 JavaScript 进程失去响应。

阅读英文原文

两种饥饿方式

第一种是长时间同步任务:循环、大排序或同步解析。在它返回之前,定时器、I/O 回调和渲染都无法运行。

第二种更隐蔽:微任务链。微任务会在下一个宏任务之前被完全清空。一个不断调度新 Promise 的 Promise 会让循环停留在微任务阶段,定时器永远不会触发。

// ❌ 饥饿:微任务队列永远不清空
function spin() {
  Promise.resolve().then(spin);
}
spin();

// ✅ 在分块之间让出事件循环
async function processInChunks(items, handle) {
  for (let i = 0; i < items.length; i += 1) {
    handle(items[i]);
    if (i % 100 === 0) {
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
}

诊断

在 Node.js 中用 perf_hooks.monitorEventLoopDelay() 测量事件循环延迟。浏览器中的长任务会出现在 Performance 面板;输入延迟和渲染间隙是用户可见症状。CPU 分析中一个长堆栈或大量 Promise 回调通常指向原因。

何时使用分块

当任务无法移到 Worker 且必须运行在主线程时,分块执行。与 Web Worker 或 worker thread 相比,分块保留共享状态简单性,但增加总耗时。与背压相比,分块在单位工作之间让出;背压限制进入系统的工作量。

陷阱

用 setTimeout(0) 让出会增加延迟;应把工作批处理到平衡响应性和吞吐量的大小。不要假设 await 会让出到 I/O:等待已解决的 Promise 仍停留在微任务队列。应在真实负载下测试;开发环境很快的循环在生产大输入下可能饥饿。

相关资源