人人都会AI编程

18.5 公共依赖抽离与包体积控制

更新时间:2026-07-11

Electron 应用被诟病最多的一点就是体积——即便是最简单的 Hello World,打包后也会轻松突破 100 MB。这是因为每个 Electron 应用都内嵌了一个完整的 Chromium 和 Node.js 运行时。但除此之外,项目自身的 node_modules 也是体积膨胀的重灾区。这一节不会去争论“体积是否真的重要”,而是直接给出几种经过验证的瘦身方案,让你的应用在分发给用户时尽可能精简。

18.5.1 理解体积的来源

在动手优化之前,先搞清楚一个 Electron 应用的安装包体积通常由哪几部分组成:

  1. Electron 运行时二进制:大约 50~70 MB(压缩后),这是硬性开销,几乎无法削减。
  2. Chromium 的资源文件:例如 locales(多语言包)、swiftshader(软件渲染库)等,大约 10~20 MB,可以通过配置剔除不用的部分。
  3. 应用自己的代码:业务逻辑、前端资源、静态文件,通常几兆到十几兆。
  4. node_modules 中的依赖:这是体积差异最大的部分。一个简单的原生模块(如 sqlite3)可能只带几十 KB 的二进制文件,但如果引入了分析工具、PDF 生成库或图像处理库,体积可能会暴涨到几百 MB。

优化重点很明确:在保证功能的前提下,尽可能减少第 2 和第 4 部分的体积

18.5.2 剔除不必要的 Electron 文件

使用 electron-builder 打包时,默认会将整个 Electron 分发版复制进去,包含很多你实际上用不到的文件。可以通过配置文件裁剪掉它们:

# electron-builder.yml
win:
  target:
    - target: nsis
      arch: [x64]
  extraResources: []  # 如果没有额外资源,可以不填

mac:
  target:
    - target: dmg
      arch: [x64, arm64]
  # 剔除掉非必要的 Chromium 资源
  files:
    - '!node_modules/**/*'
    # 只保留必要的语言包(中英文)
    - '!node_modules/electron/dist/locales/*'
    - 'node_modules/electron/dist/locales/en-US.pak'
    - 'node_modules/electron/dist/locales/zh-CN.pak'
    # 如果需要,也可以删除掉 swiftshader 等

你甚至可以写一个自定义的 afterPack 钩子脚本来进一步删除不需要的文件,比如 swiftshaderlibEGLlibGLESv2 等(如果应用不依赖 WebGL 之外的图形处理)。

18.5.3 node_modules 的公共依赖抽离

很多中大型 Electron 项目最终会打包出多个安装包(Windows × macOS × Linux),如果每个平台都独立压缩一次全量的 node_modules,在 CI 构建时间和存储空间上都是浪费。更重要的是,有些非常大的依赖(比如 puppeteer 的自带 Chromium、sharp 的预编译二进制)如果被直接打入 asar 包,会让你的应用安装包体积翻倍。

可以采用以下几种策略来抽离和控制这些依赖的体积。

1. 使用 node_modules 扁平化与 dependencies 精细化

在 Electron 项目中,务必区分 dependenciesdevDependencies只有 dependencies 中的包会被打包进最终产品,而构建工具(webpack、vite)、测试框架、类型声明都应该放在 devDependencies 中。这是一个基本但常常被忽视的细节。

进一步,可以对一些大体积的依赖进行精细控制:

  • 如果用了 puppeteer,在打包时应该指定环境变量让它在运行时使用系统已安装的 Chrome 或 Electron 自带的 Chromium,而不是额外下载一个完整的 Chromium。例如在安装依赖时设置 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true,然后在代码中通过 puppeteer.launch({ executablePath: require('electron').app.getPath('exe') }) 来复用。
  • 对于 sharp 这类图像处理原生模块,可以利用它提供的 sharp.version 选项,只保留当前平台需要的二进制(electron-builder 可以配合 postinstall 脚本自动移除其他平台的二进制文件)。

2. 共享原生依赖:使用 nodeModules 共享路径

如果你需要打包多个 Electron 应用(例如一个主程序加上多个子程序),或者同一应用的不同版本需要共享依赖,可以通过配置 electron-buildernode_modules 放置在用户系统的公共目录而不是应用安装目录内。这大大减少了每个应用的体积,并且更新依赖时可以独立于应用版本。

# electron-builder.yml
directories:
  # 将 node_modules 输出到应用的资源目录之外
  output: dist
  buildResources: build
# 使用额外的资源来引用外部 node_modules
extraResources:
  - from: node_modules
    to: ../shared_modules

然后在主进程启动时手动将 ../shared_modules 添加到 NODE_PATH,保证模块能被正常 require

// main.js
const path = require('path');
process.env.NODE_PATH = process.env.NODE_PATH
  ? process.env.NODE_PATH + path.delimiter + path.join(__dirname, '..', 'shared_modules')
  : path.join(__dirname, '..', 'shared_modules');
require('module')._initPaths();

这种方式适合同时分发多个相关应用的场景,比如一个系统托盘工具 + 设置面板的组合。

3. 借助 Webpack / Vite 进行 Tree Shaking 和 Code Splitting

现代前端打包工具在分析 requireimport 时具备强大的 tree shaking 能力,可以只把用到的模块打包进最终产物,而不是整个 node_modules。对于 Electron 的主进程和渲染进程,都可以使用打包工具来减少体积:

  • 渲染进程:常规的 Vue/React 项目已经这样做,不必多言。
  • 主进程:可以用 webpackesbuild 把主进程的 JavaScript 打包成一个单独的 bundle,并排除不需要的原生模块。
// webpack.config.js (主进程打包示例)
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: './src/main/index.js',
  target: 'node', // 把主进程当作 Node 环境打包
  externals: [
    // 排除 Electron 内置模块,避免重复打包
    nodeExternals({
      allowlist: [], // 需要打包进输出的模块
    }),
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.bundle.js',
  },
  // ...
};

打包后,你可以直接让 Electron 加载 main.bundle.jsnode_modules 中那些没有被实际使用的代码就不会再出现在应用里。

4. ASAR 压缩与拆包

默认情况下,electron-builder 会将应用的资源(包括 node_modules)打包成一个 app.asar 文件。这个 asar 归档本身不压缩,但可以通过启用压缩来减小体积:

asar: true
asarOptions:
  compression: 'maximum'  # 使用最大级别的 zlib 压缩

不过要注意,压缩后的 asar 会影响首次加载的性能,因为解压需要 CPU 时间。如果你的 node_modules 特别大(超过 200 MB),可以考虑将大的原生模块排除在 asar 之外,让它们以普通文件形式存在:

asar: true
asarOptions:
  unpack: '**/node_modules/big-library/**'

这样 big-library 不会被锁在 asar 里,Node.js 可以直接从磁盘读取它,既避免了压缩/解压开销,也防止了 asar 体积过于庞大。

18.5.4 安装包层面的优化

体积控制还需要关注最终生成的安装包(.exe/.dmg/.deb)大小。有几个实用技巧:

  • NSIS 安装包压缩(Windows)electron-builder 为 NSIS 安装器内置了 LZMA 算法,但你可以显式配置为 solid 压缩,进一步减少体积(以牺牲安装速度为代价)。
  nsis:
    oneClick: false
    allowToChangeInstallationDirectory: true
    differentialPackage: false
    installerIcon: icon.ico
    uninstallerIcon: icon.ico
    # 使用 solid 压缩可节省 10%~20%
    solid: true
  
  • macOS DMG 精简:对于 macOS,可以考虑使用 mac.zippkg 目标,有时候比 dmg 小。或者干脆使用 tar.gz 来分发,由用户自行拽入 Applications 文件夹。
  • 增量更新:如果用户已经安装了旧版本,可以通过 electron-updater 推送增量更新(delta 包),而不是每次下载完整的安装包。虽然这不改变首次安装的体积,但能极大减轻后续更新的流量压力。

18.5.5 持续监控体积

最后,体积控制不是一次性的工作,而应该成为 CI 流程的一部分。你可以在 CI 脚本中加入一个简单的检查步骤:打包完成后测量生成的安装文件大小,并设置阈值告警。例如:

SIZE=$(stat -c%s "dist/YourApp-1.0.0.exe")
if [ $SIZE -gt 157286400 ]; then  # 150 MB
  echo "Error: Installer size exceeds 150 MB"
  exit 1
fi

这样就能在体积突然膨胀时立即发现,通常是某个依赖升级后引入了不必要的文件。

总的来说,公共依赖抽离与包体积控制的核心思路是“能不带的就不带,能共享的就共享,能压缩的就压缩”。这些方法虽然不能把 Electron 应用变成几兆的小工具,但可以让你的产品在分发和用户体验上更加轻盈。对于团队而言,每一兆的体积节省都意味着更快的下载速度、更低的带宽成本和更好的用户第一印象。