浏览器多进程架构

本节目标

为什么要多进程:早期浏览器是单进程的——一个页面卡死或崩溃,整个浏览器跟着死。多进程的核心价值是隔离:把不同职责、不同站点放进各自的「盒子」(进程),一个盒子炸了,别的盒子照常工作。

Chrome 的主要进程(来自浏览器架构):

浏览器进程(Browser)   :界面、地址栏、书签、网络调度、文件访问——全局只有一个
渲染进程(Renderer)   :每个标签页(或每个站点)一个,负责解析 HTML/CSS、执行 JS、布局绘制
GPU 进程              :把图层交给显卡做合成与绘制(硬件加速)
网络进程(Network)    :统一处理所有网络请求
插件 / 工具进程        :把扩展程序隔离出去,防止扩展拖垮浏览器

渲染进程内部还有线程(这是后面事件循环、性能章节的伏笔):

主线程(Main)     :解析 HTML/CSS、执行 JS、计算布局、绘制——JS 和渲染共用它
合成线程(Compositor):把图层拼成最终画面,滚动/动画常由它处理,不阻塞主线程
光栅线程(Raster)  :把图层转成位图(像素)

关键认知:JS 运行在主线程上,而主线程同时还要做解析、布局、绘制。所以一段很长的 JS 任务会「堵住」渲染——这正是后续「事件循环」与「性能优化」要解决的问题。

站点隔离(Site Isolation):现代 Chrome 不是「一个标签页一个渲染进程」,而是「一个站点一个渲染进程」。即使一个标签页里嵌了多个不同站点的 iframe,它们也各自独立。这既能防 Spectre 这类侧信道攻击,也能避免恶意页面偷读兄弟页面的内存。

下面这段 Node 代码,用「多进程」真实演示上面的隔离思想:主进程开两个「子标签」,杀掉其中一个,另一个和主进程都还在。

// 运行环境:Node.js 14+(无需任何第三方依赖)
// 步骤:把下面内容存为 brw-l1.js,终端执行  node brw-l1.js
// 你会看到:tab2 崩溃被杀了,但 tab1 和主进程依然活着 —— 这就是「进程隔离」

const { spawn } = require('node:child_process')

// 模拟一个「标签页」子进程:spawn 开一个带 IPC 通道的独立 Node 进程
function openTab(name) {
  const child = spawn(process.execPath, ['-e',
    // 子进程启动后,监听主进程发来的「自杀」指令
    `process.on('message', (m) => { if (m === 'crash') { console.log('[${name}] 崩溃了,进程退出'); process.exit(1) } }); console.log('[${name}] 正常工作中...'); setInterval(() => {}, 1000)`
  ], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'] })
  child.on('exit', (code) => {
    if (code !== 0) console.log(`>>> 主进程收到:${name} 已退出(但主进程和其他标签不受影响)`)
  })
  return child
}

const tab1 = openTab('tab1')
const tab2 = openTab('tab2')

// 3 秒后,让 tab2「崩溃」
setTimeout(() => {
  console.log('--- 模拟 tab2 崩溃 ---')
  tab2.send('crash')
}, 3000)

// 再 2 秒后,证明主进程和 tab1 仍然健康
setTimeout(() => {
  console.log('主进程还活着,tab1 也还活着。隔离成功。')
  tab1.send('crash')
  process.exit(0)
}, 5500)

在真实浏览器里怎么看:打开 Chrome → 右上角菜单 → 更多工具 → 任务管理器,你就能看到一个个独立的「进程」和它们各自占用的内存/CPU。

名词解释

进程(Process):操作系统分配资源的最小单位,拥有独立的内存空间。你可以把它理解成「一个带围墙的院子」——院子之间不能直接翻墙互看内存。方法/能力:fork()(操作系统创建子进程)、进程间通过「进程间通信(IPC)」交换消息、一个进程崩溃不会直接拖垮另一个。

线程(Thread):进程内部的「执行流」,同一进程里的线程共享同一块内存。可以看作「院子里的多个工人,共用院子里的材料」。方法:start()(启动线程)、join()(等它跑完)、yield()(主动让出 CPU)。JS 的主线程就是渲染进程里的一条线程。

站点隔离(Site Isolation):浏览器把「不同源站点」放进不同渲染进程的安全策略。它的核心方法/手段是「按源分配进程」+「跨进程内存不可互访」,目的是把安全边界从「标签页」细化到「站点」。

课后练习

练习 1:为什么「一个标签页里打开的恶意 iframe」不会直接偷走同标签页里银行页面的内存数据?

答案:因为站点隔离后,每个站点(即使是同一标签页里的不同 iframe)都运行在独立的渲染进程中,进程之间有操作系统的内存隔离。恶意 iframe 的进程无法直接读取银行页面进程的内存,必须由浏览器按规则通过 IPC 跨进程传递数据,而这类跨源读取会被浏览器拦截。

练习 2:既然多进程更安全更稳定,为什么浏览器不把「每一个 JS 函数」都放进独立进程?

答案:进程隔离的代价是「创建慢 + 内存占用大 + 通信成本高」。如果每个函数都开进程,光创建进程的开销就拖垮性能。所以浏览器只在「进程级」做隔离(稳定性/安全),而「线程级」的细粒度调度(如把长任务拆分)交给 JS 运行时去优化——成本和收益要平衡。

总结

我认为初学者最容易把「多进程」当成一句口号背下来,但真正重要的是理解它背后的取舍思维:隔离换来稳定和安全,代价是内存和通信开销。浏览器把隔离放在「进程」这一层,而不是「函数」或「标签页」层,是一个精心权衡的结果——既挡住了「一个页面崩溃全死」和「跨站偷内存」这两类最致命的问题,又没让开销失控。学这一节,你其实是在学「大型系统如何划分边界」。后面你会反复看到同一个思想:JS 和渲染挤在「主线程」这条窄路上会堵车,而事件循环、Web Worker、合成线程,本质上都是「给主线程分流」的不同手段。