文章目录

Electron 线程与进程学习笔记

适合目标:系统掌握 Electron 中的进程模型、线程模型、大量计算应该放到哪里、如何设计通信链路,以及如何在真实项目里做隔离、解耦和性能优化。
学习重点:主进程、渲染进程、Preload、Worker、Node worker_threadschild_process、Electron utilityProcess、Native 线程与 IPC 通信。
学习原则:先分清进程和线程,再做任务分配;先理解“什么不能放在 UI 线程”,再决定用 Worker 还是子进程;先把通信模型设计清楚,再谈性能。

说明:这篇笔记结合了 2026 年 4 月 21 日核对的官方资料整理。Electron 官方文档明确写到 utilityProcess 适合承载 CPU 密集型任务、非可信服务和易崩溃组件;Node.js 官方文档明确写到 worker_threads 适合 CPU 密集型 JavaScript 计算,而不太适合单纯 I/O 密集型任务。


目录

  1. 学习总览
  2. Electron 为什么必须学进程和线程
  3. 先分清进程和线程
  4. Electron 的核心进程模型
  5. 主进程、渲染进程、Preload 到底分别干什么
  6. Electron 中常见的“重任务”类型
  7. 大量计算到底该放哪里
  8. worker_threads 什么时候用
  9. child_process 什么时候用
  10. utilityProcess 什么时候用
  11. Native Addon / C++ 线程什么时候用
  12. 通信方式总表
  13. 常见通信链路设计
  14. 一个推荐的任务调度架构
  15. 线程和进程使用中的常见坑
  16. 性能优化建议
  17. 实战架构建议
  18. 高频面试题
  19. 官方资料入口
  20. 一页速记总结
  21. 背诵口诀

1. 学习总览

1.1 Electron 里这部分到底在学什么

很多人做 Electron 时最容易出现的一个问题是:

  1. 所有逻辑都堆在主进程
  2. 页面里直接做大量计算
  3. 渲染卡了才想起要拆线程

Electron 的线程和进程,不是“优化项”,很多时候是系统设计基础。

你真正要学的是:

在 Electron 里,哪些工作应该留在 UI,哪些工作应该放到后台线程,哪些工作应该放到独立进程,以及它们之间如何稳定通信。

1.2 这部分最核心的 3 个问题

  1. 大量计算不能放哪
  2. 大量计算应该放哪
  3. 结果怎么安全地传回来

2. Electron 为什么必须学进程和线程

因为 Electron 同时具备:

  1. 桌面应用的复杂度
  2. 浏览器页面的渲染模型
  3. Node.js 的异步与并发模型
  4. 原生扩展能力

这意味着你很容易同时遇到:

  1. UI 卡顿
  2. 主进程阻塞
  3. 多窗口之间相互影响
  4. 大计算拖垮应用
  5. 原生模块阻塞事件循环

一句话:

Electron 不是只有页面,也不是只有 Node,它是一个多进程、多模型混合环境。


3. 先分清进程和线程

3.1 进程是什么

进程可以理解成:

一个独立运行的程序实例,拥有独立的内存空间。

特点:

  1. 隔离强
  2. 崩一个不一定全崩
  3. 通信成本更高

3.2 线程是什么

线程可以理解成:

进程内部的执行单元,共享同一进程的大部分资源。

特点:

  1. 通信更快
  2. 共享内存更方便
  3. 但更容易出现竞争和阻塞问题

3.3 一句话区别

  1. 进程更重,隔离更强
  2. 线程更轻,协作更快

4. Electron 的核心进程模型

Electron 官方的 Process Model 文档主线非常重要。

4.1 Main Process

主进程是 Electron 应用的控制中心。

负责:

  1. 应用生命周期
  2. 窗口管理
  3. 菜单、托盘、通知
  4. IPC 注册
  5. 系统能力协调

4.2 Renderer Process

渲染进程负责页面渲染和用户交互。

可以理解成:

跑在 Chromium 里的页面进程

4.3 Preload

Preload 不是独立进程,但它是很关键的一层。

负责:

  1. 安全桥接
  2. 暴露白名单 API
  3. 收口底层能力

4.4 Utility Process

Electron 官方文档明确说明,utilityProcess 是从主进程派生出来的子进程,运行在 Node.js 环境里,适合:

  1. CPU 密集型任务
  2. 不可信服务
  3. 易崩溃组件

而且它支持 MessagePort 通信。


5. 主进程、渲染进程、Preload 到底分别干什么

5.1 主进程应该做什么

  1. 应用调度
  2. IPC 中转
  3. 系统级操作
  4. 子进程 / Utility Process 管理
  5. 原生模块统一接入

5.2 主进程不应该做什么

  1. 长时间 CPU 密集计算
  2. 大量同步阻塞 I/O
  3. 复杂 UI 逻辑

5.3 渲染进程应该做什么

  1. UI 展示
  2. 交互处理
  3. 轻量业务逻辑
  4. 状态管理

5.4 渲染进程不应该做什么

  1. 超重计算
  2. 阻塞性循环
  3. 无边界调用系统能力

5.5 Preload 应该做什么

  1. 暴露白名单接口
  2. 收敛 IPC
  3. 做薄薄的一层桥接

5.6 Preload 不应该做什么

  1. 重计算
  2. 大量业务逻辑
  3. 复杂状态中心

6. Electron 中常见的“重任务”类型

不是所有重任务都一样,要先分类。

6.1 CPU 密集型

例如:

  1. 大量 JSON 解析和聚合
  2. 图片处理
  3. 音视频处理
  4. 加密解密
  5. 大数据计算
  6. AI 推理

6.2 I/O 密集型

例如:

  1. 文件读写
  2. 网络请求
  3. 数据库查询
  4. 系统命令

6.3 易崩溃 / 非可信组件

例如:

  1. 第三方原生库
  2. 非稳定脚本任务
  3. 非可信插件

6.4 原生重任务

例如:

  1. C++ 算法
  2. 编解码
  3. 硬件设备 SDK

7. 大量计算到底该放哪里

这是最核心的一节。

7.1 最简决策图

任务来了
  -> 是 UI 渲染相关吗?
      -> 是:留在 Renderer,但要轻
      -> 否:
         -> 是 JS 纯计算且 CPU 密集吗?
             -> 是:优先 worker_threads
         -> 是独立服务或高风险任务吗?
             -> 是:优先 utilityProcess / child_process
         -> 是原生 C++ / 编解码 / SDK 吗?
             -> 是:Native Addon + 后台线程 / 独立进程
         -> 是普通 I/O 吗?
             -> 放主进程或专门 service 层,用异步 API

7.2 一个务实结论

  1. UI 相关轻逻辑留在 Renderer
  2. CPU 密集型 JS 计算优先 worker_threads
  3. 高隔离/高风险/独立脚本任务优先 utilityProcess
  4. 外部程序或 CLI 任务用 child_process
  5. 原生重任务放 Native Addon / C++ 线程 / 独立进程

8. worker_threads 什么时候用

Node.js 官方文档明确说:

worker_threads 适合 CPU-intensive JavaScript operations,不太适合单纯 I/O-intensive work。

8.1 适用场景

  1. 大量 JSON / AST 计算
  2. 本地复杂文本解析
  3. 图像像素级 JS 处理
  4. 大批量数据计算
  5. 需要共享内存的 JS 计算

8.2 优势

  1. 比进程更轻
  2. 启动成本更低
  3. 可以通过 ArrayBuffer / SharedArrayBuffer 共享内存

8.3 不适合场景

  1. 只是普通异步文件读写
  2. 需要强隔离和防崩溃
  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 通信方式

  1. workerData
  2. parentPort.postMessage
  3. MessageChannel
  4. SharedArrayBuffer

9. child_process 什么时候用

Node.js 官方文档里,child_process 是标准子进程能力。

9.1 适用场景

  1. 调用本地命令行工具
  2. 执行 ffmpeg / python / cli 工具
  3. 启动独立服务
  4. 需要完全独立内存空间

9.2 常见方式

  1. spawn
  2. exec
  3. execFile
  4. fork

9.3 什么时候更偏向它

  1. 你已经有现成 exe / cli
  2. 想做强隔离
  3. 不在乎进程启动开销

9.4 短板

  1. 启动更重
  2. 通信比线程麻烦
  3. 资源管理更复杂

10. utilityProcess 什么时候用

Electron 官方文档明确把 utilityProcess 定位为:

  1. 从 Main 启动的 Node.js 子进程
  2. 可用 MessagePort 通信
  3. 适合 CPU 密集型任务、非可信服务、易崩溃组件

10.1 为什么它在 Electron 里很重要

因为这是 Electron 官方给出的、更贴近桌面应用架构的“后台独立进程”方案。

10.2 适用场景

  1. 大计算但希望隔离主进程
  2. 第三方高风险逻辑
  3. 可能崩溃的解析器/转换器
  4. 独立后台服务模块

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 适用场景

  1. 音视频编解码
  2. 图像处理
  3. 加密
  4. 设备 SDK
  5. 高性能算法

11.2 为什么不能简单理解成“C++ 就快”

如果你把原生任务接到了主进程的事件循环上,还是会卡。

11.3 正确思路

  1. 原生层负责高性能实现
  2. 耗时逻辑在原生后台线程执行
  3. 结果异步回调给 JS

11.4 什么时候甚至要原生独立进程

  1. 原生库不稳定
  2. 崩溃风险高
  3. 占用资源太重

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 为什么不要页面直接连所有后台执行单元

因为这样会:

  1. 安全面扩散
  2. 通信模型混乱
  3. 后续难维护

13.3 推荐任务路由层

建议在 Main 里抽一个 task schedulercompute service

  1. 接收任务
  2. 决定丢给谁执行
  3. 管理并发和取消
  4. 汇总结果和错误

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 这样做的价值

  1. 统一入口
  2. 任务路由清晰
  3. 后续能做线程池、进程池
  4. 容易限流和监控

15. 线程和进程使用中的常见坑

15.1 在 Renderer 里直接跑大循环

后果:

  1. 页面卡死
  2. 动画掉帧
  3. 输入响应迟缓

15.2 在 Main 里做同步重计算

后果:

  1. 整个应用响应变差
  2. 多窗口都受影响

15.3 Worker 和 Process 乱用

例如:

  1. 只是异步 I/O 也强上 Worker
  2. 本可以线程解决却开很多子进程

15.4 通信数据太大

问题:

  1. 序列化成本高
  2. 内存峰值高
  3. 卡顿反而更严重

15.5 忘记做任务取消和超时

后果:

  1. 页面关了任务还在跑
  2. 进程泄漏
  3. 内存越来越高

15.6 崩溃恢复没设计

尤其是:

  1. utility process 崩了怎么办
  2. 原生任务崩了怎么办
  3. 子进程卡死怎么办

16. 性能优化建议

16.1 任务分级

把任务分成:

  1. UI 即时任务
  2. 普通异步任务
  3. CPU 重任务
  4. 高风险隔离任务

16.2 做池化而不是无限创建

  1. worker pool
  2. process pool

16.3 控制并发数

不要因为能开线程/进程就疯狂开。

16.4 大数据传输尽量减少拷贝

优先考虑:

  1. ArrayBuffer
  2. Transferable 对象
  3. 分块传输

16.5 监控关键指标

  1. 主进程卡顿时间
  2. 渲染进程长任务
  3. worker 执行时长
  4. utility process 存活与退出码
  5. 任务队列长度

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 一个推荐的落地原则

  1. 页面只发任务,不自己做重计算
  2. 主进程只路由和调度,不扛重任务
  3. JS 重计算走 worker_threads
  4. 高风险和隔离任务走 utilityProcess
  5. 原生重任务走 Addon / C++ 后台线程

17.3 如果问“大量计算放到哪里”

最推荐的务实回答是:

  1. 纯 JS CPU 密集型任务优先放 worker_threads
  2. 高风险或需要独立隔离的任务优先放 utilityProcess
  3. 原生性能任务放 Native Addon / C++ 后台线程
  4. 页面和主进程都不应该直接承担长时间重计算

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_threadschild_process 的区别

  1. Worker 是线程,更轻,适合 JS 计算
  2. Child process 是进程,更重,隔离更强

18.7 大量计算到底放哪里最合适

  1. 纯 JS 重计算:worker_threads
  2. 隔离需求高:utilityProcess / child_process
  3. 原生高性能任务:Native Addon / C++

18.8 Electron 中通信怎么设计更合理

推荐通过 Preload 暴露白名单 API,由 Main 做统一任务调度,再把任务分发给 Worker、Utility Process 或 Native 层。


19. 官方资料入口

  1. Electron Process Model
  2. Electron utilityProcess
  3. Node.js worker_threads
  4. Node.js child_process

20. 一页速记总结

20.1 决策口诀

  1. UI 留 Renderer
  2. 调度留 Main
  3. JS 重计算走 worker_threads
  4. 高风险隔离走 utilityProcess
  5. 原生性能任务走 C++ / Addon

20.2 最重要的一句话

主进程和渲染进程都不该长期扛重计算,它们更应该做协调和展示。


21. 背诵口诀

页面只管展示和交互,Preload 负责白名单,Main 负责路由和调度;JS 重任务丢给 Worker,高风险任务丢给 Utility,原生重活交给 C++,通信统一走桥接。