技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

从浏览器多进程到 JavaScript 单线程:JS 运行机制梳理

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/11-从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
本文目录15 个章节
  1. 一、为什么要把这些概念放在一起?
  2. 二、进程和线程
  3. 三、浏览器是多进程、多线程的
  4. 四、Browser 进程和 Renderer 进程如何协作?
  5. 五、渲染进程中的主要工作角色
  6. 六、GUI 渲染线程与 JavaScript 线程“互斥”该如何理解?
  7. 七、Web Worker:JavaScript 的独立执行环境
  8. 八、浏览器渲染流程
  9. 九、普通绘制和合成层
  10. 十、从 Event Loop 理解 JavaScript 运行
  11. 十一、定时器到底由什么控制?
  12. 十二、事件循环进阶:Promise 和 MutationObserver
  13. 十三、如何把知识用于性能优化?
  14. 总结
  15. 参考资料

从浏览器多进程到 JavaScript 单线程:JS 运行机制梳理

Category(分类): Browser Status: 已更新

原文试图把浏览器多进程、渲染进程、Web Worker、渲染流程、事件循环、定时器和宏任务/微任务串成一条知识链。本文保留这条主线和大部分历史示意图,同时修正“一个 Tab 必然一个进程”“每个定时器都有独立线程”“GUI 线程和 JS 线程绝对互斥”“SharedWorker 必然独立进程”等过度简化的说法。

一、为什么要把这些概念放在一起?

在浏览器中输入 URL 后,网络、进程、线程、HTML 解析、JavaScript 执行、样式计算、布局、绘制、合成和事件循环会相互配合。面试题常常从“浏览器是多进程的”追问到“为什么 setTimeout 不准时”,再追问到“微任务什么时候执行”。

本文按以下顺序梳理:

  • 区分进程和线程;
  • 认识浏览器的多进程架构;
  • 认识渲染进程中的主线程、合成和 Worker;
  • 复习浏览器渲染流程以及 DOMContentLoaded/load
  • 理解任务、微任务和渲染机会;
  • 认识定时器的最小延迟、后台节流和 setInterval 的局限。

二、进程和线程

进程通常拥有独立的虚拟地址空间、资源和安全边界;线程是进程内的执行单元,同一进程中的线程可以共享部分内存。教材常把进程称为资源分配单位、线程称为调度单位,这个说法便于入门,但实际资源和调度由操作系统决定。

进程与线程的关系示意

可以用工厂来类比:

进程像一个有边界的工厂,拥有自己的资源。
线程像工厂里的工人,多个工人可以协作完成任务。
同一工厂的工人可以共享仓库,但需要遵守同步规则。
不同工厂相互隔离,通信需要经过额外的通道。

任务管理器可以展示某一时刻的进程、CPU 和内存信息,但不同操作系统和浏览器版本的展示方式不同。一个浏览器窗口、标签页或站点不应直接等同于一个进程。

三、浏览器是多进程、多线程的

现代浏览器通常会把不同职责放进不同的进程或服务中。以 Chromium 的概念模型为例:

  • Browser 进程:浏览器 UI、窗口和标签页管理、权限、历史记录、进程协调等;
  • Renderer 进程:运行网页脚本、构建 DOM、样式计算、布局和绘制准备;
  • Network Service:网络请求和协议相关工作;
  • GPU/Viz 相关进程:图形设备、合成和显示;
  • Utility、Storage、音视频、扩展等进程:根据功能和安全边界动态创建。

浏览器多个标签页的进程示意

浏览器多个进程示意

1. “一个 Tab 一个 Renderer”只是粗略模型

早期文章常说“每打开一个 Tab 就会新建一个渲染进程”。现在 Chromium 会根据站点隔离、同源关系、页面策略、内存压力、扩展和版本实现决定进程归属:

  • 一个页面可能使用多个相关进程;
  • 多个页面在满足条件时可能共享一个渲染进程;
  • 高风险跨站内容可能被放入不同进程;
  • 浏览器可能冻结、丢弃或重新创建后台页面。

因此,进程隔离确实有助于稳定性和安全性,但不能用固定进程数量推断所有浏览器行为。

浏览器进程合并或复用示意

2. 多进程的优点和代价

优点:

  • 某个页面或扩展崩溃时,通常不会直接拖垮整个浏览器;
  • 通过沙盒、权限和站点隔离降低跨站攻击的影响范围;
  • 网络、页面脚本、图形和浏览器 UI 可以利用多核并行工作;
  • 浏览器可以单独回收闲置页面和相关资源。

代价:

  • 进程、线程、IPC 和图形表面都会消耗资源;
  • 页面之间传输数据需要序列化、共享内存或其他 IPC 机制;
  • 多进程不等于页面一定更快,过多进程也可能增加内存压力。

四、Browser 进程和 Renderer 进程如何协作?

下面是简化的导航模型:

  1. Browser 进程接收用户输入、标签页操作和导航请求;
  2. 网络服务负责 DNS、连接、HTTP 和资源获取;
  3. 浏览器根据站点隔离和当前页面状态准备或复用渲染进程;
  4. 渲染进程解析 HTML、执行脚本、计算样式、布局并产生绘制记录;
  5. 合成和显示相关进程把页面表面提交到窗口;
  6. 进程之间通过 IPC、共享内存和浏览器定义的接口交换消息。

Browser 进程与 Renderer 进程通信示意

这不是“Renderer 直接把一张 Bitmap 交给 Browser 进程”的固定实现。现代 Chromium 的导航提交、网络服务、合成器和 Viz 之间有更细的接口,浏览器版本升级后流程也可能变化。

五、渲染进程中的主要工作角色

1. 页面主线程

主线程通常负责:

  • HTML 解析和 DOM 构建;
  • CSS 解析、样式计算;
  • JavaScript 执行、事件回调和微任务;
  • 布局计算、绘制记录和部分资源处理。

同一个 Window JavaScript agent 的脚本按顺序执行,因此长任务会阻塞同一主线程上的输入、样式、布局和绘制准备。

2. 合成线程和栅格化线程

合成线程可以处理部分滚动和合成动画。绘制记录还需要经过图块化、栅格化和纹理管理,可能由工作线程、CPU 或 GPU 路径共同完成。浏览器内部存在很多并行工作,但这不等于 JavaScript DOM 代码可以同时在多个线程上执行。

3. 事件、定时器和网络回调

HTML 标准定义的是事件何时进入任务队列,不要求浏览器一定为鼠标事件、XHR、setTimeout 各创建一个固定线程。网络和计时通常由浏览器基础设施完成,条件满足后把回调排入对应事件循环的任务队列。

六、GUI 渲染线程与 JavaScript 线程“互斥”该如何理解?

旧资料说“GUI 渲染线程与 JS 引擎线程互斥”,它表达了一个重要事实:页面主线程不能在执行一段 JavaScript 的同时又在同一线程上执行另一段布局或绘制代码。因此,长脚本会挡住页面更新。

但现代浏览器并不存在一个可以简单概括全部场景的“GUI 线程被 JS 线程冻结”模型:

  • 合成线程可以独立处理部分滚动和合成;
  • 网络、图片解码和栅格化可以在其他线程或进程完成;
  • 主线程执行 JavaScript 时,其他线程仍可能工作;
  • DOM、样式和布局的协调仍然主要由页面主线程完成。

更准确的结论是:长时间占用页面主线程会阻塞依赖主线程的工作,但不会阻止浏览器所有线程做任何事情。

七、Web Worker:JavaScript 的独立执行环境

Worker 为 Web 内容提供后台 JavaScript 执行环境:

// main.js
const worker = new Worker('/worker.js', { type: 'module' })

worker.postMessage({ values: [1, 2, 3, 4] })
worker.addEventListener('message', event => {
  console.log(event.data)
})

// worker.js
self.addEventListener('message', event => {
  const total = event.data.values.reduce((sum, value) => sum + value, 0)
  self.postMessage({ total })
})

Worker 的关键特征:

  • 有自己的全局对象和事件循环;
  • 不能直接访问页面 DOM;
  • 可以通过结构化克隆、可转移对象或共享内存通信;
  • 创建线程和复制数据存在成本,不能把所有任务都搬进 Worker。

如果需要传输大块二进制数据,可以转移 ArrayBuffer 的所有权:

const buffer = new ArrayBuffer(1024)
worker.postMessage(buffer, [buffer])
// 转移后主线程不再拥有这个 ArrayBuffer 的可用内容

Web Worker 和 SharedWorker

  • Worker 通常由创建它的页面使用,页面关闭或主动终止后生命周期结束;
  • SharedWorker 可以让同源的多个页面连接到同一个 SharedWorkerGlobalScope
  • 共享 worker 是否运行在独立操作系统进程中由浏览器决定,规范不提供这一保证;
  • Service Worker 也有独立生命周期和事件模型,不能简单等同于普通 Worker。

八、浏览器渲染流程

为了简化理解,页面大致会经过:

  1. 解析 HTML,构建 DOM;
  2. 解析 CSS,构建 CSSOM 并计算样式;
  3. 生成布局树,计算节点尺寸和位置;
  4. 生成绘制记录;
  5. 根据变换、滚动、重叠和滤镜等因素组织图层;
  6. 将绘制记录分块并栅格化;
  7. 合成图层并显示。

浏览器渲染流程示意

浏览器会缓存和增量更新这些结果,不是每次修改都从 DOM 解析重新开始。transformopacity 等属性在条件合适时可能只需要更新合成参数,但不能把它们当作规范保证。

DOMContentLoadedload

  • DOMContentLoaded:HTML 解析完成,并且需要等待的延迟脚本、模块脚本执行后触发;通常不等待普通图片和视频;
  • load:文档及其需要参与初始加载的子资源完成后触发;懒加载和交互后加载的资源不应机械地算入初始等待;
  • 一般情况下 DOMContentLoaded 早于 load,但二者都不是 LCP、INP 等用户体验指标。

CSS 是否阻塞 DOM 解析?

外部样式表通常不会让 HTML 解析器停止读取后续 HTML,但它可能阻塞渲染,并使经典同步脚本、延迟脚本或模块脚本等待样式准备。原因是脚本可以读取和修改样式,浏览器需要维护一致的执行结果。

九、普通绘制和合成层

现代浏览器内部会维护绘制块、图层树、图块和合成表面等结构。不能简单理解为“普通文档流是一个固定默认层、每个绝对定位元素都有一层”。是否创建独立合成表面取决于内容和浏览器决策。

页面图层和合成示意

可能促使浏览器使用合成资源的情况包括:

  • transformopacity 动画;
  • 视频、canvas、部分 iframe;
  • 固定定位、滚动容器、滤镜和复杂重叠;
  • 开发者提供的 will-change 提示。

translateZ(0)translate3d(0, 0, 0) 并不是现代浏览器通用的“开启 GPU”开关。图层越多,纹理内存、栅格化、上传和合成成本越高。

使用 DevTools 的 Performance、Rendering 和 Layers 面板验证实际效果,比背诵“硬件加速属性列表”更可靠。

十、从 Event Loop 理解 JavaScript 运行

在一个 Window agent 中,可以先用下面的简化模型理解:

  • 同步代码在当前任务中执行,形成调用栈;
  • 浏览器事件、网络完成、定时器到期等会排队形成任务;
  • 当前任务结束后,执行微任务检查点;
  • 浏览器在合适的时机处理渲染更新;
  • 再从任务队列中选择下一个可执行任务。

任务队列和调用栈示意

事件循环演讲中的调用栈与任务队列示意

console.log('script start')

setTimeout(() => {
  console.log('timer task')
}, 0)

Promise.resolve().then(() => {
  console.log('promise microtask')
})

console.log('script end')

常见输出顺序是:

script start
script end
promise microtask
timer task

因为当前脚本任务结束后,微任务会在下一个计时器任务之前执行。微任务也不能无限递归,否则会造成微任务饥饿,让浏览器长时间没有机会处理输入和渲染。

任务和微任务

**任务(task)**包括一轮脚本执行、计时器回调、用户事件回调、网络事件等。**微任务(microtask)**常见来源包括 Promise reaction、queueMicrotask() 和 MutationObserver 回调。

规范中的事件循环并不保证“每个 task 执行后一定立即渲染”。浏览器会根据刷新率、页面可见性、任务类型和自身调度策略决定渲染机会。因此应把下面理解为常见模型,而不是硬性顺序:

task -> microtask checkpoint -> 可能更新渲染 -> 下一个 task

宏任务、微任务和渲染机会示意

如果当前任务持续很久,微任务也会等到当前任务结束;如果微任务不断产生新的微任务,浏览器也可能迟迟无法进入渲染阶段。

Node.js 还拥有自己的事件循环实现。process.nextTick() 是 Node.js API,不是浏览器标准的微任务 API;在 Node.js 中它有特殊的优先级,不能直接套用浏览器任务队列的所有细节。

十一、定时器到底由什么控制?

调用 setTimeout 后,浏览器会根据 HTML 定时器算法在满足时间条件时安排一个任务。规范没有要求浏览器一定创建一个名为“定时器线程”的独立线程。真正执行回调时,仍然要等待当前任务和更高优先级工作完成。

setTimeout(() => {
  console.log('至少在指定延迟之后,且在主线程有机会时执行')
}, 0)

console.log('begin')

输出一定先出现 begin,因为当前脚本任务尚未结束。0 不是立即执行,也不是精确的 0ms 保证。

4ms 最小延迟和后台节流

旧文章常说“W3C 规定所有小于 4ms 的定时器都按 4ms 处理”。现在的 HTML 定时器规则主要针对嵌套定时器层级超过阈值的情况;此外浏览器还会对后台页面、隐藏页面和资源紧张场景进行节流,具体策略因浏览器和平台而异。不要把 4ms 当成所有定时器的统一精度。

setTimeoutsetIntervalrequestAnimationFrame

setInterval 试图按给定周期安排任务,但回调的实际开始时间会受到主线程忙碌、后台节流和页面生命周期影响,也不适合保证网络轮询不重叠。需要避免前一次任务未完成就安排下一次时,可以使用递归 setTimeout

let stopped = false

async function poll() {
  if (stopped) return

  try {
    await fetch('/api/status', { cache: 'no-store' })
  } finally {
    if (!stopped) {
      setTimeout(poll, 5000)
    }
  }
}

function stop() {
  stopped = true
}

poll()

动画和视觉更新优先使用 requestAnimationFrame,它会让浏览器在下一次绘制前通知页面:

let rafId = 0

function renderFrame(time) {
  box.style.transform = `translateX(${time / 10}px)`
  rafId = requestAnimationFrame(renderFrame)
}

rafId = requestAnimationFrame(renderFrame)
// 需要停止时:cancelAnimationFrame(rafId)

不要在页面隐藏时依赖定时器高频运行;后台页面通常会被节流。需要可靠的服务端任务时,应放在服务端或任务系统中。

十二、事件循环进阶:Promise 和 MutationObserver

Promise 的回调属于微任务,通常在当前任务结束后执行:

console.log('script start')

setTimeout(() => console.log('setTimeout'), 0)

Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'))

console.log('script end')

结果通常为:

script start
script end
promise1
promise2
setTimeout

MutationObserver 回调也在微任务检查点执行,过去一些框架会用它实现 nextTick 的降级策略。现在的框架和浏览器兼容代码会优先使用 Promise、queueMicrotask 或 MessageChannel,具体选择属于框架实现细节。不要依赖某个旧版本 Vue 源码的内部 fallback 推断所有现代 Vue 版本。

十三、如何把知识用于性能优化?

  • 使用 Performance 面板区分长任务、Style、Layout、Paint、Raster 和 Composite;
  • 使用 PerformanceObserver、Long Tasks 和 Web Vitals 记录真实用户体验;
  • 通过 requestAnimationFrame 安排视觉更新;
  • 通过 setTimeoutscheduler.yield() 或 Worker 拆分 CPU 密集任务;
  • 避免读写布局信息交错造成强制同步布局;
  • 不要滥用 will-change 和人为创建图层;
  • DOMContentLoaded/load 了解生命周期,但用 LCP、INP、CLS 衡量用户体验;
  • 对脚本选择 deferasync、模块和动态导入时,先分析依赖关系和失败降级。

总结

  • 浏览器是多进程、多线程系统,页面与进程不是固定的一对一关系;
  • 一个页面的 JavaScript agent 通常串行执行,Worker 提供独立的执行环境;
  • 主线程长任务会阻塞依赖它的输入、样式、布局和绘制准备;
  • CSS、脚本和渲染之间存在依赖,但“CSS 阻塞一切”“GUI 线程绝对冻结”都是过度简化;
  • 任务结束后先处理微任务,浏览器再在合适时机更新渲染;
  • 定时器是延迟后排队任务,不是精确时钟,也不保证有独立线程;
  • setInterval 不适合处理不能重叠的任务,动画应优先考虑 requestAnimationFrame
  • 性能优化必须通过浏览器工具、真实设备和真实用户数据验证。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS