进程模型深入
本教程共 45 篇 · 第 43 篇 · 更新于 2026-08-03
43. 进程模型深入
本节目标
- 理解 Electron 继承自 Chromium 的多进程架构。
- 知道何时该用多个渲染进程。
- 掌握 utility process 的适用场景。
- 了解渲染进程崩溃隔离的原理。
- 弄清进程之间的边界与权限划分。
1-1 为什么是多进程,而不是单进程
早期的浏览器几乎都用单个进程承载所有标签页。这种方式开销小,但代价是一个网页崩溃或卡死,整个浏览器就跟着挂掉。Chromium 团队改用多进程架构:每个标签页跑在独立渲染进程里,由一个浏览器进程统一调度。某个标签页出错,只影响它自己。
Electron 的架构与此高度相似。作为开发者,你主要掌控两类进程:主进程(main)与渲染进程(renderer),它们分别对应 Chromium 的浏览器进程和渲染进程。这种隔离正是 Electron 稳定性与安全性的一大根基。
1-2 多个渲染进程的分工
每创建一个 BrowserWindow,Electron 就会为其派生一个独立的渲染进程;BrowserView、WebContentsView、<webview> 这类内嵌网页同样会各自占用渲染进程。不同窗口之间天然隔离,一个窗口崩溃不会连累另一个。
多窗口的常见用法有两种。一是「主窗口 + 子窗口」,比如设置面板、关于窗口,这类窗口通常设 parent 关联,关闭父窗口时跟随关闭。二是「多独立窗口」,例如一个聊天应用同时打开多个会话窗口,每个窗口是独立渲染进程。
// 主进程:创建两个互相独立的窗口
const { BrowserWindow } = require('electron')
const winA = new BrowserWindow({ width: 800, height: 600 })
winA.loadFile('page-a.html')
const winB = new BrowserWindow({ width: 400, height: 300, parent: winA })
winB.loadFile('page-b.html')
需要注意:每个渲染进程都是独立的内存与执行上下文,窗口之间不能直接共享变量。要互通数据,要么走主进程做中转(主进程是单一数据源),要么用 MessagePort 建立直接的消息通道。
1-3 utility process:把重活挪出主进程
主进程一旦卡住,整个应用都会失去响应;渲染进程崩溃了还能单独恢复。那么 CPU 密集、易崩溃、或来自不可信来源的任务,应该放在哪里?答案之一是 utility process(工具进程)。
通过 utilityProcess.fork,你可以从主进程派生一个运行在 Node.js 环境里的子进程。它和 Node 的 child_process.fork 很像,但底层用 Chromium 的 Services API 启动,关键差异是它能通过 MessagePort 与渲染进程直接建立通信通道,而不必绕主进程中转。适合放进去的包括:不可信服务、CPU 密集型计算、容易崩溃的组件。
// 主进程:派生一个工具进程
const { utilityProcess, MessageChannelMain } = require('electron')
const path = require('node:path')
const { port1, port2 } = new MessageChannelMain()
const child = utilityProcess.fork(path.join(__dirname, 'worker.js'))
// 把 port1 交给子进程,port2 留在主进程
child.postMessage({ msg: 'start' }, [port1])
child.on('message', (e) => {
console.log('来自工具进程:', e.message)
})
// 工具进程 worker.js
process.parentPort.on('message', (e) => {
console.log('收到主进程消息:', e.data)
// 执行耗时或易崩溃的逻辑
})
utilityProcess.fork 只能在 app 的 ready 事件之后调用。子进程可以通过 child.kill() 优雅终止,用 child.pid 判断是否在运行,用 child.stdout/stderr 读取输出,还能监听 spawn、exit、error 等事件。
1-4 渲染进程崩溃隔离
崩溃隔离是多进程架构最直观的收益。当某个渲染进程因内存耗尽或脚本错误而崩溃时,Electron 会触发 render-process-gone 事件,你可以据此弹出提示、尝试重载,而不必关闭整个应用。
// 主进程:监听渲染进程崩溃
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ /* ... */ })
win.webContents.on('render-process-gone', (event, details) => {
console.error('渲染进程崩溃:', details.reason)
// 可选:重载页面或提示用户
win.webContents.reload()
})
与之相对,主进程崩溃往往是致命的,因为窗口与应用生命周期都由它掌控。因此把「不稳定因素」尽量下沉到渲染进程或工具进程,是提升健壮性的有效策略。
1-5 进程边界与权限划分
不同进程拥有的权限差别很大,理解边界才能既安全又高效:
- 主进程:完整 Node.js 环境,能调用全部原生模块与系统 API,但无沙箱保护,必须谨慎处理不可信内容。
- 渲染进程:默认没有 Node 能力,受上下文隔离与沙箱约束;即使崩溃也影响有限。
- 预加载脚本:运行在渲染进程的独立上下文,保有 Node 能力但受沙箱限制,是安全桥接点。
- 工具进程:独立 Node 环境,可承载重计算或不可信服务,并通过
MessagePort与主/渲染通信。
一个重要的安全结论是:不要把不可信内容放进主进程或无沙箱的渲染进程。加载不可信远程内容时,应放在已启用 nodeIntegration: false、contextIsolation: true、sandbox: true 的渲染进程或 <webview> 里。
1-6 何时该拆分更多进程
并不是进程越多越好,派生与通信都有成本。下面这些信号提示你该拆进程了:
- 某段逻辑 CPU 占用高、会卡住界面,应放进工具进程。
- 某个组件历史上频繁崩溃,隔离出去以免影响主窗口。
- 需要运行不可信或第三方代码,用工具进程并配合
disclaim(macOS)降低归属风险。 - 多窗口各自独立、互不依赖,本就应是不同渲染进程。
反之,如果只是简单的数据中转或轻量计算,留在主进程、用 IPC 处理通常更省事。
还有一个实用手段:用 app.getAppMetrics() 可以拿到每个进程(包括渲染进程与工具进程)的 CPU、内存占用等运行指标。当你发现某个窗口或任务异常吃资源时,先查指标定位到具体进程,再决定是把逻辑拆出去、还是优化算法,而不是凭感觉盲调。进程模型的价值,最终都要落到「可观测、可隔离、可恢复」这三件事上。
小结
Electron 的多进程架构是它的稳定与安全基石。多个渲染进程天然隔离、崩溃互不影响;utility process 让你把重活和不可信代码挪出主进程,还能用 MessagePort 直连渲染进程。把握「主进程集中能力、渲染进程负责界面、工具进程承担风险」的分工,应用的健壮性和可扩展性都会上一个台阶。下一章我们看 Electron 自身的版本演进与升级策略。