Electron 线程与进程学习笔记
适合目标:系统掌握 Electron 中的进程模型、线程模型、大量计算应该放到哪里、如何设计通信链路,以及如何在真实项目里做隔离、解耦和性能优化。
学习重点:主进程、渲染进程、Preload、Worker、Nodeworker_threads、child_process、ElectronutilityProcess、Native 线程与 IPC 通信。
学习原则:先分清进程和线程,再做任务分配;先理解“什么不能放在 UI 线程”,再决定用 Worker 还是子进程;先把通信模型设计清楚,再谈性能。
说明:这篇笔记结合了 2026 年 4 月 21 日核对的官方资料整理。Electron 官方文档明确写到
utilityProcess适合承载 CPU 密集型任务、非可信服务和易崩溃组件;Node.js 官方文档明确写到worker_threads适合 CPU 密集型 JavaScript 计算,而不太适合单纯 I/O 密集型任务。
目录
- 学习总览
- Electron 为什么必须学进程和线程
- 先分清进程和线程
- Electron 的核心进程模型
- 主进程、渲染进程、Preload 到底分别干什么
- Electron 中常见的“重任务”类型
- 大量计算到底该放哪里
worker_threads什么时候用child_process什么时候用utilityProcess什么时候用- Native Addon / C++ 线程什么时候用
- 通信方式总表
- 常见通信链路设计
- 一个推荐的任务调度架构
- 线程和进程使用中的常见坑
- 性能优化建议
- 实战架构建议
- 高频面试题
- 官方资料入口
- 一页速记总结
- 背诵口诀
1. 学习总览
1.1 Electron 里这部分到底在学什么
很多人做 Electron 时最容易出现的一个问题是:
- 所有逻辑都堆在主进程
- 页面里直接做大量计算
- 渲染卡了才想起要拆线程
Electron 的线程和进程,不是“优化项”,很多时候是系统设计基础。
你真正要学的是:
在 Electron 里,哪些工作应该留在 UI,哪些工作应该放到后台线程,哪些工作应该放到独立进程,以及它们之间如何稳定通信。
1.2 这部分最核心的 3 个问题
- 大量计算不能放哪
- 大量计算应该放哪
- 结果怎么安全地传回来
2. Electron 为什么必须学进程和线程
因为 Electron 同时具备:
- 桌面应用的复杂度
- 浏览器页面的渲染模型
- Node.js 的异步与并发模型
- 原生扩展能力
这意味着你很容易同时遇到:
- UI 卡顿
- 主进程阻塞
- 多窗口之间相互影响
- 大计算拖垮应用
- 原生模块阻塞事件循环
一句话:
Electron 不是只有页面,也不是只有 Node,它是一个多进程、多模型混合环境。
3. 先分清进程和线程
3.1 进程是什么
进程可以理解成:
一个独立运行的程序实例,拥有独立的内存空间。
特点:
- 隔离强
- 崩一个不一定全崩
- 通信成本更高
3.2 线程是什么
线程可以理解成:
进程内部的执行单元,共享同一进程的大部分资源。
特点:
- 通信更快
- 共享内存更方便
- 但更容易出现竞争和阻塞问题
3.3 一句话区别
进程更重,隔离更强线程更轻,协作更快
4. Electron 的核心进程模型
Electron 官方的 Process Model 文档主线非常重要。
4.1 Main Process
主进程是 Electron 应用的控制中心。
负责:
- 应用生命周期
- 窗口管理
- 菜单、托盘、通知
- IPC 注册
- 系统能力协调
4.2 Renderer Process
渲染进程负责页面渲染和用户交互。
可以理解成:
跑在 Chromium 里的页面进程
4.3 Preload
Preload 不是独立进程,但它是很关键的一层。
负责:
- 安全桥接
- 暴露白名单 API
- 收口底层能力
4.4 Utility Process
Electron 官方文档明确说明,utilityProcess 是从主进程派生出来的子进程,运行在 Node.js 环境里,适合:
- CPU 密集型任务
- 不可信服务
- 易崩溃组件
而且它支持 MessagePort 通信。
5. 主进程、渲染进程、Preload 到底分别干什么
5.1 主进程应该做什么
- 应用调度
- IPC 中转
- 系统级操作
- 子进程 / Utility Process 管理
- 原生模块统一接入
5.2 主进程不应该做什么
- 长时间 CPU 密集计算
- 大量同步阻塞 I/O
- 复杂 UI 逻辑
5.3 渲染进程应该做什么
- UI 展示
- 交互处理
- 轻量业务逻辑
- 状态管理
5.4 渲染进程不应该做什么
- 超重计算
- 阻塞性循环
- 无边界调用系统能力
5.5 Preload 应该做什么
- 暴露白名单接口
- 收敛 IPC
- 做薄薄的一层桥接
5.6 Preload 不应该做什么
- 重计算
- 大量业务逻辑
- 复杂状态中心
6. Electron 中常见的“重任务”类型
不是所有重任务都一样,要先分类。
6.1 CPU 密集型
例如:
- 大量 JSON 解析和聚合
- 图片处理
- 音视频处理
- 加密解密
- 大数据计算
- AI 推理
6.2 I/O 密集型
例如:
- 文件读写
- 网络请求
- 数据库查询
- 系统命令
6.3 易崩溃 / 非可信组件
例如:
- 第三方原生库
- 非稳定脚本任务
- 非可信插件
6.4 原生重任务
例如:
- C++ 算法
- 编解码
- 硬件设备 SDK
7. 大量计算到底该放哪里
这是最核心的一节。
7.1 最简决策图
任务来了
-> 是 UI 渲染相关吗?
-> 是:留在 Renderer,但要轻
-> 否:
-> 是 JS 纯计算且 CPU 密集吗?
-> 是:优先 worker_threads
-> 是独立服务或高风险任务吗?
-> 是:优先 utilityProcess / child_process
-> 是原生 C++ / 编解码 / SDK 吗?
-> 是:Native Addon + 后台线程 / 独立进程
-> 是普通 I/O 吗?
-> 放主进程或专门 service 层,用异步 API
7.2 一个务实结论
UI 相关轻逻辑留在 RendererCPU 密集型 JS 计算优先 worker_threads高隔离/高风险/独立脚本任务优先 utilityProcess外部程序或 CLI 任务用 child_process原生重任务放 Native Addon / C++ 线程 / 独立进程
8. worker_threads 什么时候用
Node.js 官方文档明确说:
worker_threads 适合 CPU-intensive JavaScript operations,不太适合单纯 I/O-intensive work。
8.1 适用场景
- 大量 JSON / AST 计算
- 本地复杂文本解析
- 图像像素级 JS 处理
- 大批量数据计算
- 需要共享内存的 JS 计算
8.2 优势
- 比进程更轻
- 启动成本更低
- 可以通过
ArrayBuffer/SharedArrayBuffer共享内存
8.3 不适合场景
- 只是普通异步文件读写
- 需要强隔离和防崩溃
- 外部命令执行
8.4 简化示意
// main or service layer
const { Worker } = require('node:worker_threads')
function runHeavyTask(payload) {
return new Promise((resolve, reject) => {
const worker = new Worker(require.resolve('./heavy-worker.js'), {
workerData: payload
})
worker.once('message', resolve)
worker.once('error', reject)
worker.once('exit', (code) => {
if (code !== 0) reject(new Error(`worker exited with ${code}`))
})
})
}
8.5 通信方式
workerDataparentPort.postMessageMessageChannelSharedArrayBuffer
9. child_process 什么时候用
Node.js 官方文档里,child_process 是标准子进程能力。
9.1 适用场景
- 调用本地命令行工具
- 执行 ffmpeg / python / cli 工具
- 启动独立服务
- 需要完全独立内存空间
9.2 常见方式
spawnexecexecFilefork
9.3 什么时候更偏向它
- 你已经有现成 exe / cli
- 想做强隔离
- 不在乎进程启动开销
9.4 短板
- 启动更重
- 通信比线程麻烦
- 资源管理更复杂
10. utilityProcess 什么时候用
Electron 官方文档明确把 utilityProcess 定位为:
- 从 Main 启动的 Node.js 子进程
- 可用 MessagePort 通信
- 适合 CPU 密集型任务、非可信服务、易崩溃组件
10.1 为什么它在 Electron 里很重要
因为这是 Electron 官方给出的、更贴近桌面应用架构的“后台独立进程”方案。
10.2 适用场景
- 大计算但希望隔离主进程
- 第三方高风险逻辑
- 可能崩溃的解析器/转换器
- 独立后台服务模块
10.3 相比 child_process.fork 的特点
Electron 官方文档说明,一个重要差异是:
utilityProcess 可以和 renderer 建立基于 MessagePort 的通信通道。`
10.4 推荐场景判断
如果你是在 Electron 主进程里 fork 一个 Node 后台模块,官方建议通常可以优先考虑 utilityProcess。
10.5 简化示意
const { utilityProcess, MessageChannelMain } = require('electron')
const path = require('node:path')
const child = utilityProcess.fork(path.join(__dirname, 'worker-entry.js'))
const { port1, port2 } = new MessageChannelMain()
child.postMessage({ type: 'init' }, [port1])
port2.on('message', (event) => {
console.log(event.data)
})
11. Native Addon / C++ 线程什么时候用
11.1 适用场景
- 音视频编解码
- 图像处理
- 加密
- 设备 SDK
- 高性能算法
11.2 为什么不能简单理解成“C++ 就快”
如果你把原生任务接到了主进程的事件循环上,还是会卡。
11.3 正确思路
- 原生层负责高性能实现
- 耗时逻辑在原生后台线程执行
- 结果异步回调给 JS
11.4 什么时候甚至要原生独立进程
- 原生库不稳定
- 崩溃风险高
- 占用资源太重
12. 通信方式总表
| 场景 | 推荐通信方式 | 备注 |
|---|---|---|
| Renderer <-> Main | ipcRenderer / ipcMain |
最常见 |
| Renderer <-> Preload | contextBridge |
白名单暴露 |
| Main <-> worker_threads | postMessage / MessageChannel |
同进程线程通信 |
| Main <-> child_process | stdio / IPC channel | 更传统 |
| Main <-> utilityProcess | postMessage + MessagePortMain |
Electron 官方推荐路线 |
| JS <-> Native Addon | 函数调用 / Promise / callback | 取决于 Addon 设计 |
13. 常见通信链路设计
13.1 页面发起大计算请求
推荐链路:
Renderer -> Preload API -> Main Task Router -> Worker/Utility/Native -> Main -> Renderer
13.2 为什么不要页面直接连所有后台执行单元
因为这样会:
- 安全面扩散
- 通信模型混乱
- 后续难维护
13.3 推荐任务路由层
建议在 Main 里抽一个 task scheduler 或 compute service:
- 接收任务
- 决定丢给谁执行
- 管理并发和取消
- 汇总结果和错误
13.4 任务调度流程图
Renderer 触发任务
-> Preload 白名单 API
-> Main Process Task Router
-> 轻量任务:主进程异步处理
-> CPU 密集型 JS:worker_threads
-> 高风险/独立模块:utilityProcess
-> 外部命令:child_process
-> 原生性能任务:Native Addon / C++
-> Main 汇总结果
-> 返回 Renderer 更新 UI
14. 一个推荐的任务调度架构
+-------------------------------------------------------------------+
| Electron App |
| |
| Renderer |
| |- 页面 UI |
| |- 状态管理 |
| |- 任务发起 |
| |
| Preload |
| |- expose taskApi.run() |
| |
| Main |
| |- Task Router |
| |- Task Queue |
| |- Concurrency Control |
| |- Result Cache |
| |
| Execution Backends |
| |- Async I/O Service |
| |- worker_threads Pool |
| |- utilityProcess Workers |
| |- child_process Adapter |
| |- Native Addon / C++ |
+-------------------------------------------------------------------+
14.1 这样做的价值
- 统一入口
- 任务路由清晰
- 后续能做线程池、进程池
- 容易限流和监控
15. 线程和进程使用中的常见坑
15.1 在 Renderer 里直接跑大循环
后果:
- 页面卡死
- 动画掉帧
- 输入响应迟缓
15.2 在 Main 里做同步重计算
后果:
- 整个应用响应变差
- 多窗口都受影响
15.3 Worker 和 Process 乱用
例如:
- 只是异步 I/O 也强上 Worker
- 本可以线程解决却开很多子进程
15.4 通信数据太大
问题:
- 序列化成本高
- 内存峰值高
- 卡顿反而更严重
15.5 忘记做任务取消和超时
后果:
- 页面关了任务还在跑
- 进程泄漏
- 内存越来越高
15.6 崩溃恢复没设计
尤其是:
- utility process 崩了怎么办
- 原生任务崩了怎么办
- 子进程卡死怎么办
16. 性能优化建议
16.1 任务分级
把任务分成:
- UI 即时任务
- 普通异步任务
- CPU 重任务
- 高风险隔离任务
16.2 做池化而不是无限创建
- worker pool
- process pool
16.3 控制并发数
不要因为能开线程/进程就疯狂开。
16.4 大数据传输尽量减少拷贝
优先考虑:
ArrayBuffer- Transferable 对象
- 分块传输
16.5 监控关键指标
- 主进程卡顿时间
- 渲染进程长任务
- worker 执行时长
- utility process 存活与退出码
- 任务队列长度
17. 实战架构建议
17.1 推荐目录结构
src/
main/
ipc/
scheduler/
taskRouter.ts
workerPool.ts
utilityPool.ts
preload/
renderer/
workers/
computeWorker.js
utility/
utilityEntry.js
native/
addon/
17.2 一个推荐的落地原则
- 页面只发任务,不自己做重计算
- 主进程只路由和调度,不扛重任务
- JS 重计算走
worker_threads - 高风险和隔离任务走
utilityProcess - 原生重任务走 Addon / C++ 后台线程
17.3 如果问“大量计算放到哪里”
最推荐的务实回答是:
- 纯 JS CPU 密集型任务优先放
worker_threads - 高风险或需要独立隔离的任务优先放
utilityProcess - 原生性能任务放 Native Addon / C++ 后台线程
- 页面和主进程都不应该直接承担长时间重计算
18. 高频面试题
18.1 Electron 的主进程和渲染进程区别是什么
主进程负责应用生命周期、窗口与系统能力调度;渲染进程负责页面 UI 和用户交互。
18.2 Electron 里为什么不能把大量计算放在 Renderer
因为会阻塞页面渲染和交互,导致卡顿、掉帧和无响应。
18.3 Electron 里为什么不能把大量计算都放 Main
因为主进程负责整个应用调度,阻塞它会影响窗口响应、IPC 和系统能力调用。
18.4 worker_threads 适合什么
Node.js 官方文档明确指出它适合 CPU 密集型 JavaScript 计算,不太适合仅仅 I/O 密集型任务。
18.5 utilityProcess 适合什么
Electron 官方文档明确指出它适合 CPU 密集型任务、不可信服务和易崩溃组件,而且适合从主进程 fork 出后台独立进程。
18.6 worker_threads 和 child_process 的区别
- Worker 是线程,更轻,适合 JS 计算
- Child process 是进程,更重,隔离更强
18.7 大量计算到底放哪里最合适
- 纯 JS 重计算:
worker_threads - 隔离需求高:
utilityProcess/child_process - 原生高性能任务:Native Addon / C++
18.8 Electron 中通信怎么设计更合理
推荐通过 Preload 暴露白名单 API,由 Main 做统一任务调度,再把任务分发给 Worker、Utility Process 或 Native 层。
19. 官方资料入口
20. 一页速记总结
20.1 决策口诀
UI 留 Renderer调度留 MainJS 重计算走 worker_threads高风险隔离走 utilityProcess原生性能任务走 C++ / Addon
20.2 最重要的一句话
主进程和渲染进程都不该长期扛重计算,它们更应该做协调和展示。
21. 背诵口诀
页面只管展示和交互,Preload 负责白名单,Main 负责路由和调度;JS 重任务丢给 Worker,高风险任务丢给 Utility,原生重活交给 C++,通信统一走桥接。