本文章由 cladue opus 4.5 润色。
最近 Vibe coding 了一个 AI 桌面编程助手,基于 Electron + Vite。最近发现安装包膨胀到 180MB 了,下载体验很差,于是花了点时间做了一轮系统性优化。
最终成果:DMG 从 180MB 降到 80MB,app.asar 从 416MB 降到 56MB。没改任何业务代码。
这篇文章记录整个过程。
最有效
1. 把 renderer-only 依赖移到 devDependencies(asar -267MB)
这是整个优化里收益最大的一步,效果超过其他所有步骤加起来。
原理很简单:应用用 Vite 打包 renderer 进程,产出的是一个自包含的 bundle(20MB),运行时根本不需要 node_modules 里的东西。但 electron-builder 打包时会把 dependencies 里所有包都塞进 asar——不管你 bundle 用没用到。
所以像 @lobehub/ui、react、zustand、lucide-react 这些只在 renderer 里用的包,全部应该放到 devDependencies。
npm install --save-dev @lobehub/ui react react-dom zustand lucide-react一行命令,asar 从 323MB 直接掉到 56MB。
electron-builder 打包会把 dependencies 里的包连带整个依赖树一起打进去。这两个过程是独立的。
2. electron-builder 开启最大压缩(DMG -23MB)
一行配置的事:
{
"build": {
"compression": "maximum"
}
}默认是 normal,改成 maximum 后 DMG 立减 14%。代价是构建时间稍微长一点,但对发布构建来说完全可以接受。
3. afterPack 钩子清理垃圾文件(unpacked -19MB)
Electron 用 native 模块(比如 better-sqlite3)时,需要把 .node 文件从 asar 里解压出来。问题是解压的时候会把整个模块目录都带出来,包括一堆运行时根本不需要的东西:
sqlite3.c源码(9MB)README、LICENSE、CHANGELOG
.gyp、Makefile等构建文件
另外 Electron 框架自带 70 多种语言的 locale 文件,每个几百 KB,应用只需要保留一种即可(参考 innei 播客)。
用 afterPack 钩子在打包后、签名前把这些都清掉:
exports.default = async function(context) {
// 清理 native 模块里的源码和文档
const junkPatterns = ['**/*.c', '**/*.h', '**/README.md', '**/*.gyp']
// ... 删除匹配的文件
// 只保留英文 locale
const keepLocales = ['en.lproj']
// ... 删除其他 locale 目录
}unpacked 目录从 21MB 降到 2MB,DMG 也跟着小了 12MB。
中等收益的优化
这几个不如上面三个效果明显,但也值得做:
移除未使用的依赖(asar -87MB)
全局搜一下 import 语句,发现 antd、react-markdown 这些包装了但根本没用到。Web 项目里这不是大问题(Vite 会 tree-shake 掉),但 Electron 会把整个 node_modules 打进去,所以该删还是得删。
Vite 构建配置
esbuild: {
drop: ['console', 'debugger'] // 生产环境移除调试代码
}renderer bundle 能小个几百 KB。
试了但没啥用的
记录一下,省得踩同样的坑:
精确配置 asarUnpack 的 glob 模式
我以为把 glob 写精确点,就能只解压 .node 文件而不带上源码:
"asarUnpack": ["**/better-sqlite3/build/Release/*.node"]
然而并没有用。electron-builder 的逻辑是:glob 匹配到哪个模块,就把整个模块目录都解压出来。想精确控制解压内容,只能用 afterPack 钩子事后清理。
静态资源压缩
把 logo.png 从 228KB 压到 26KB,icon.icns 从 1.2MB 压到 656KB。但经过 asar 打包和 DMG 压缩后,在 MB 级别的测量精度下基本看不出差异。属于代码卫生层面的事,对最终体积影响不大。
files 配置排除 .map 文件
"files": ["!out/**/*.map"]
配了,但当前构建本来就没生成这些文件,所以没有实际收益。算是防御性配置。
各步骤收益汇总
| 优化项 | DMG | asar | unpacked |
|---|---|---|---|
| renderer 依赖移到 devDep | -49MB | -267MB | — |
| 最大压缩配置 | -23MB | -12MB | — |
| afterPack 清理 | -12MB | — | -19MB |
| 移除未使用依赖 | -16MB | -87MB | — |
| Vite 构建优化 | — | -12MB | — |
| 累计 | -100MB | -360MB | -19MB |
一些经验
每步都要打包验证。不要一口气做完所有优化再测,而是改一步、打包一次、记录数据。这样才能知道哪步真正有效、哪步是白忙活。我们就是靠这个方法发现 asarUnpack 优化其实没用的。
构建有波动。相同代码多次打包,asar 体积可能有 ±6-12MB 的波动,别被误导了。
Electron 的依赖管理比 Web 敏感得多。Web 应用只打包 import 的代码,Electron 会把整个 node_modules 带上。定期审计依赖是必要的。
剩下的优化空间主要在 Electron 框架本身(比如 16MB 的 Vulkan 渲染器我们其实用不到)和 renderer bundle 的懒加载(mermaid 只在渲染图表时需要)。这些后面再搞。