首页 / Electron 入门教程 / 进程模型深入

Electron 入门教程

进程模型深入

本教程共 45 篇 · 第 43 篇 · 更新于 2026-08-03

Electron进程模型多渲染进程utility process崩溃隔离权限边界

43. 进程模型深入

本节目标

  • 理解 Electron 继承自 Chromium 的多进程架构。
  • 知道何时该用多个渲染进程。
  • 掌握 utility process 的适用场景。
  • 了解渲染进程崩溃隔离的原理。
  • 弄清进程之间的边界与权限划分。

1-1 为什么是多进程,而不是单进程

早期的浏览器几乎都用单个进程承载所有标签页。这种方式开销小,但代价是一个网页崩溃或卡死,整个浏览器就跟着挂掉。Chromium 团队改用多进程架构:每个标签页跑在独立渲染进程里,由一个浏览器进程统一调度。某个标签页出错,只影响它自己。

Electron 的架构与此高度相似。作为开发者,你主要掌控两类进程:主进程(main)与渲染进程(renderer),它们分别对应 Chromium 的浏览器进程和渲染进程。这种隔离正是 Electron 稳定性与安全性的一大根基。

1-2 多个渲染进程的分工

每创建一个 BrowserWindow,Electron 就会为其派生一个独立的渲染进程;BrowserViewWebContentsView<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 只能在 appready 事件之后调用。子进程可以通过 child.kill() 优雅终止,用 child.pid 判断是否在运行,用 child.stdout/stderr 读取输出,还能监听 spawnexiterror 等事件。

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: falsecontextIsolation: truesandbox: true 的渲染进程或 <webview> 里。

1-6 何时该拆分更多进程

并不是进程越多越好,派生与通信都有成本。下面这些信号提示你该拆进程了:

  • 某段逻辑 CPU 占用高、会卡住界面,应放进工具进程。
  • 某个组件历史上频繁崩溃,隔离出去以免影响主窗口。
  • 需要运行不可信或第三方代码,用工具进程并配合 disclaim(macOS)降低归属风险。
  • 多窗口各自独立、互不依赖,本就应是不同渲染进程。

反之,如果只是简单的数据中转或轻量计算,留在主进程、用 IPC 处理通常更省事。

还有一个实用手段:用 app.getAppMetrics() 可以拿到每个进程(包括渲染进程与工具进程)的 CPU、内存占用等运行指标。当你发现某个窗口或任务异常吃资源时,先查指标定位到具体进程,再决定是把逻辑拆出去、还是优化算法,而不是凭感觉盲调。进程模型的价值,最终都要落到「可观测、可隔离、可恢复」这三件事上。

小结

Electron 的多进程架构是它的稳定与安全基石。多个渲染进程天然隔离、崩溃互不影响;utility process 让你把重活和不可信代码挪出主进程,还能用 MessagePort 直连渲染进程。把握「主进程集中能力、渲染进程负责界面、工具进程承担风险」的分工,应用的健壮性和可扩展性都会上一个台阶。下一章我们看 Electron 自身的版本演进与升级策略。