浏览器事件循环:从进程模型到任务队列
梳理渲染主线程如何调度任务、异步为何必要、JS 为何会阻塞渲染,以及 Chrome 中延时/交互/微队列的优先级。
浏览器事件循环:从进程模型到任务队列
JavaScript 常被说成「单线程」,但真正决定页面是否流畅的,是浏览器里那条繁忙的 渲染主线程。它既要解析 HTML/CSS、布局绘制,又要执行 JS 与事件回调——这些任务如何排队、谁先谁后,就是 事件循环(消息循环) 要回答的问题。
下文从浏览器进程模型讲起,再到异步模型、阻塞渲染的实测,以及 W3C 与 Chrome 对任务队列的最新理解。
浏览器的进程与线程
进程是什么?
程序运行需要专属的内存空间,这块空间可以理解为 进程。每个应用至少有一个进程;进程之间相互独立,若要通信也需要双方同意。

线程是什么?
有了进程,才能在其中运行代码。执行代码的单元叫 线程。进程启动后会自动创建一个线程运行代码,称为 主线程;若需并行执行多段代码,进程内还可再开更多线程。

浏览器有哪些进程?
浏览器是多进程、多线程应用。 启动后会拉起多个进程,以降低相互影响和连环崩溃的风险。可在浏览器任务管理器中查看当前进程列表。

与前端开发最相关的是 渲染进程:启动后会开启 渲染主线程,负责执行 HTML、CSS、JavaScript。默认情况下,每个标签页对应一个渲染进程,标签页之间互不影响。(Chrome 的进程模型仍在演进,详见 Chrome 官方说明。)
渲染主线程如何工作?
渲染主线程是浏览器中最繁忙的线程,典型工作包括:
- 解析 HTML、CSS
- 计算样式、布局、处理图层
- 以约 60fps 重绘页面
- 执行全局 JS、事件处理函数、计时器回调
- ……
这么多任务如何调度?例如:JS 执行到一半时用户点了按钮,要立即处理点击吗?计时器到时了,要立即跑回调吗?点击与计时器同时到来,先处理谁?
主线程的做法是:排队。

- 渲染主线程进入一个 不会结束的循环(事件循环)。
- 每次循环检查 消息队列:有任务则取出第一个执行,执行完再进入下一轮;没有任务则休眠。
- 其他线程(含其他进程的线程)可随时把任务加到队列 末尾;若主线程在休眠,会被唤醒继续取任务。
整条链路也叫 事件循环 或 消息循环。
思考题:为什么渲染进程不用多个线程并行做解析、布局、绘制?
简答:DOM 与样式状态需要一致的可预测顺序;多线程并行改同一棵 DOM 树会带来同步与竞态难题,单主线程 + 队列调度是更稳妥的工程取舍(GPU 合成等仍可由其他线程辅助)。
异步:主线程不能阻塞
有些任务 无法立刻完成,例如:
- 计时器:
setTimeout、setInterval - 网络:
XHR、Fetch - 用户操作:
addEventListener
若主线程一直等到「时间到了」「请求返回」「用户点击」,就会长期阻塞,页面卡死:

渲染主线程承担页面更新与交互,绝不能被长时间阻塞。浏览器因此采用 异步:

主线程把计时、网络、监听等交给其他线程,自己继续执行后续代码;待时机成熟,将回调包装成任务加入消息队列末尾,由主线程在空闲时调度。异步模式下,主线程不会因等待 I/O 而空转阻塞。
如何理解 JS 的异步?(面试要点)
- JS 运行在渲染主线程中,同一时刻只有一条执行栈,故常被称为单线程语言。
- 主线程还要负责渲染;若大量任务同步等待,队列里其他任务无法执行,页面无法及时更新。
- 异步做法:计时、网络、事件等交由其他线程;完成后把回调入队,主线程按序执行。
JS 为何会阻碍渲染?
改 DOM 与真正画到屏幕上并非同一瞬间完成。若主线程正在执行一段 长时间占用的 JS,渲染任务只能等在队列后面。
主线程阻塞导致画面延迟更新
html<h1>Hello, Event Loop!</h1>
<button>change</button>
<script>
const h1 = document.querySelector('h1')
const btn = document.querySelector('button')
function delay(duration) {
const start = Date.now()
while (Date.now() - start < duration) {}
}
btn.onclick = function () {
h1.textContent = '页面已更新!'
delay(3000)
}
</script>点击按钮后:textContent 虽已修改,但 画面要等 delay(3000) 跑完 才会更新——因为渲染步骤也要主线程空闲才能执行。
下方可亲自点击体验(约 3 秒内标题不会变,之后才刷新):
JS 阻塞渲染
Live Preview · 可编辑代码
任务有优先级吗?
单个任务在所属队列里通常是先进先出,没有「插队」;但 队列之间 有优先级差异。
W3C 现行表述要点(Perform a microtask checkpoint):
- 每个任务有 任务类型;同类型任务在同一队列,不同类型可分到不同队列。
- 一次事件循环中,浏览器可按情况从不同队列取任务。
- 浏览器必须维护 微队列(microtask queue),其中任务优先于其他队列执行。
随着浏览器复杂度提升,简单区分「宏队列 / 微队列」已不足以描述全部行为,但 微任务优先清空 仍是硬性要求。
在 Chrome 实现中,至少包括:
| 队列 | 典型内容 | 优先级(相对) |
|---|---|---|
| 微队列 | Promise.then、MutationObserver 等 | 最高 |
| 交互队列 | 用户操作产生的事件处理 | 高 |
| 延时队列 | setTimeout / setInterval 到期回调 | 中 |
立即把函数放进微队列的常见写法:
Promise 入微队列
javascriptPromise.resolve().then(() => {
console.log('microtask')
})常见面试题
阐述 JS 的事件循环
事件循环是渲染主线程的工作方式:开启不会结束的循环,每次从队列取一个任务执行;其他线程在合适时机把任务加到队列末尾。任务按类型分队列,浏览器在一次循环中决定取哪个队列;微队列任务必须优先调度执行。Chrome 源码层面可理解为 for 循环 + 队列取任务,而非过去教科书里单一的「宏队列」模型。
计时器能精确计时吗?
不能做到严格精确,原因包括:
- 硬件与 OS 计时本身存在误差,JS 计时器最终依赖系统调用。
- W3C 规定:嵌套超过 5 层的计时器,最小间隔为 4ms,短于 4ms 的设定会被抬高。
- 回调只在主线程空闲时执行,前面若有长任务,到期时间到了也会 晚跑。
小结
- 浏览器多进程隔离标签页;渲染主线程 是页面与 JS 的核心调度点。
- 事件循环 用消息队列 + 无限循环保证任务有序执行。
- 异步 避免主线程等待 I/O;长同步 JS 仍会阻塞渲染与交互。
- 理解 微队列优先 与多队列模型,比死记「宏/微」更符合当前规范与 Chrome 行为。