Electron 应用体积从 190MB+ 到 90MB
摘要
我有一个 Electron + asar: true、调整 build.files 但效果不明显。最终我改用“从 release 产物倒推”的方法定位体积大头,发现真正的问题是 渲染端依赖(antd/react 等)被以 node_modules 形式打进 app.asar。通过把渲染端依赖从 dependencies 迁移到 devDependencies,并只构建 x64,最终把安装包压到了 90MB+,app.asar 从 90MB 降到 4.89MB。
本文记录完整排查路径、关键命令、最终配置与注意事项,可直接复用。
背景:为什么 Electron 打包这么大?
Electron 的体积主要由两部分组成:
- Electron Runtime(Chromium +
Node.js ):固定成本,通常几十到上百 MB。 - 你的应用资源(app.asar / resources):可控,包含业务代码、前端构建产物、资源文件、依赖等。
瘦身的核心:让 “可控部分” 足够小;同时避免把不该打进生产的东西(尤其是 node_modules)塞进包里。
第 1 步:从 release/ 入手,先找“最大头”
不要猜,直接量化。
看安装包体积
release/ 里 installer 大小(例):
- BinLockPro-Setup-1.1.0.exe:约 205MB(优化前)
看 win-unpacked 里各文件大小(PowerShell)
Get-ChildItem -Path ".\release\win-unpacked" -File |
ForEach-Object { [PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($_.Length/1MB,2)} } |
Sort-Object SizeMB -Descending |
Select-Object -First 15 |
Format-Table -AutoSize2
3
4
5
我当时看到最异常的一条:
BinLockPro.exe200MB(非常大)
同时 resources/ 目录也不小,所以继续看 resources:
Get-ChildItem -Path ".\release\win-unpacked\resources" |
ForEach-Object {
if($_.PSIsContainer) {
$size=(Get-ChildItem $_.FullName -Recurse -File | Measure-Object Length -Sum).Sum
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($size/1MB,2); Type="Dir"}
} else {
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($_.Length/1MB,2); Type="File"}
}
} |
Sort-Object SizeMB -Descending |
Format-Table -AutoSize2
3
4
5
6
7
8
9
10
11
关键发现:
- app.asar 90MB(异常大)
到这一步,结论已经很清晰:业务代码不可能 90MB,90MB 大概率是依赖被“塞进 asar”了。
第 2 步:验证 app.asar 里到底塞了什么
列出 asar 内容(不解包也行)
npx asar list ".\release\win-unpacked\resources\app.asar" | Select-Object -First 50结果一眼看到:
\node_modules\...
这就坐实了:node_modules 被打进了 app.asar。
进一步统计 node_modules 谁是大头(解包)
解包会生成临时文件夹,建议放到 release 的 temp 目录,方便清理。
npx asar extract ".\release\win-unpacked\resources\app.asar" ".\release\temp_asar_extract"看顶层体积分布:
Get-ChildItem -Path ".\release\temp_asar_extract" -Directory |
ForEach-Object {
$size=(Get-ChildItem $_.FullName -Recurse -File | Measure-Object Length -Sum).Sum
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($size/1MB,2)}
} | Sort-Object SizeMB -Descending | Format-Table -AutoSize2
3
4
5
结果(优化前):
node_modules85MBh51.xMBsrc0.xMB
继续看 node_modules 里大头是谁:
Get-ChildItem -Path ".\release\temp_asar_extract\node_modules" -Directory |
ForEach-Object {
$size=(Get-ChildItem $_.FullName -Recurse -File | Measure-Object Length -Sum).Sum
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($size/1MB,2)}
} | Sort-Object SizeMB -Descending | Select-Object -First 20 | Format-Table -AutoSize2
3
4
5
当时前几名非常典型:
antd50MB+@ant-design10MB+react-dom4MB+@supabase、@tanstack等
第 3 步:根因解释——为什么这些依赖不应该进入生产包?
这个项目结构是 Electron 主进程 + Vite 渲染进程:
- 渲染进程:React/antd 等,最终会被
Vite打到h5/(也就是静态资源)。 - 主进程:Electron runtime 加载
h5/,主进程真正需要的运行时依赖通常很少(如electron-store、electron-updater等)。
但是 electron-builder 默认行为是:
- 把 dependencies 当作生产运行时依赖,一并打包(进入 asar / app)
- devDependencies 不会打进最终包(仅构建时使用)
所以当你把 react、antd 这种“渲染端构建期依赖”放在 dependencies 时,electron-builder 就会把它们整包塞进 asar —— 这就是 app.asar 90MB 的根源。
第 4 步:关键修复——重划 dependencies / devDependencies
目标
- dependencies:只保留 主进程运行时真的要用 的库
- devDependencies:渲染端依赖、构建工具、仅开发时用的东西
我最终的迁移策略(示例)
把下面这些迁到 devDependencies:
- react / react-dom
- antd / @ant-design/icons
- i18next / react-i18next
- zustand
- @tanstack/react-query
- @supabase/supabase-js
- cross-env(它只影响脚本环境变量,不需要进生产包)
dependencies 保留(示例):
- electron-store
- electron-updater
- uuid
注意:如果你的主进程确实
require()了某些库(例如@supabase/supabase-js在主进程调用),那它就必须留在 dependencies。我的项目里它属于渲染端使用,所以迁走没问题。
第 5 步:额外优化——只构建 x64,避免双架构包
如果你 win.target.arch 同时配置了 x64 + ia32,产物会更复杂,体积与构建时间都会上升。
只保留 x64:
{
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
]
}
}
我的package.json关键配置(build):
```json
"build": {
"appId": "xxx",
"compression": "maximum",
"asar": true,
"productName": "xxx",
"copyright": "xxx",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"src/main/**/*",
"src/utils/**/*",
"src/config/**/*",
"src/preload/**/*",
"src/assets/**/*",
"h5/**/*",
"package.json"
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"icon": "src/assets/app.ico",
"artifactName": "${productName}-${version}.${ext}"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "BinLockPro",
"installerIcon": "src/assets/app.ico",
"uninstallerIcon": "src/assets/app.ico",
"installerHeaderIcon": "src/assets/app.ico",
"artifactName": "${productName}-Setup-${version}.${ext}"
},
"mac": {
"target": "dmg",
"icon": "src/assets/icon.icns",
"category": "public.app-category.productivity"
},
"linux": {
"target": [
"AppImage",
"deb"
],
"icon": "src/assets/icon.png",
"category": "Utility"
},
"publish": {
"provider": "github",
"owner": "xxx",
"repo": "xxx"
}
},2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
第 6 步:验证结果(强烈建议写进你的检查清单)
重新安装依赖并重新打包
建议做一次干净构建(避免旧依赖干扰):
- 删除
release/ - 删除
node_modules/ npm installnpm run make
验证 app.asar 是否显著变小
到安装目录或 release/win-unpacked/resources 查看:
- app.asar:从 90MB 降到 4.89MB(成功)
安装包体积变化
安装包从 190MB+ 降到 90MB+。
为什么安装后 最终程序包 还是 200MB?正常吗?
正常。
最终程序包 是 Electron Runtime 本体(Chromium + Node.js + 各类 runtime 数据),属于 Electron 固定成本,并不是你的业务代码。
你能优化的是:
resources/app.asarresources/app.asar.unpacked(如果你用asarUnpack)locales/resources.pak等(可进一步裁剪,但收益有限且要谨慎)
如果你希望安装后体积也“很小”(比如 10MB 级别),那就需要换技术栈(例如 Tauri / Wails / 原生等),这属于架构层面取舍。
最终总结:最有效的瘦身原则
原则 1:从产物倒推,不猜
先在 release/win-unpacked 看文件大小,锁定最大头,再深入分析 asar 内容。
原则 2:Vite 项目里,渲染端依赖不要留在 dependencies
除非主进程真的需要,否则 react/antd 这类都应放在 devDependencies。
原则 3:asar 变小才算真正优化成功
app.asar 从 90MB → 5MB,这才是“应用可控部分”瘦身的标志。
原则 4:Electron runtime 大小是固定成本
.exe 大,不等于你打包错了;主要看 app.asar 和 installer 体积。
附:我这次用到的排查命令合集(可复制)
# 1) 查看 win-unpacked 最大文件
Get-ChildItem -Path ".\release\win-unpacked" -File |
ForEach-Object { [PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($_.Length/1MB,2)} } |
Sort-Object SizeMB -Descending |
Select-Object -First 15 |
Format-Table -AutoSize
# 2) 查看 resources 下各文件大小
Get-ChildItem -Path ".\release\win-unpacked\resources" |
ForEach-Object {
if($_.PSIsContainer) {
$size=(Get-ChildItem $_.FullName -Recurse -File | Measure-Object Length -Sum).Sum
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($size/1MB,2); Type="Dir"}
} else {
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($_.Length/1MB,2); Type="File"}
}
} |
Sort-Object SizeMB -Descending |
Format-Table -AutoSize
# 3) 列出 asar 内容
npx asar list ".\release\win-unpacked\resources\app.asar" | Select-Object -First 50
# 4) 解包 asar
npx asar extract ".\release\win-unpacked\resources\app.asar" ".\release\temp_asar_extract"
# 5) 统计 asar 解包后顶层目录大小
Get-ChildItem -Path ".\release\temp_asar_extract" -Directory |
ForEach-Object {
$size=(Get-ChildItem $_.FullName -Recurse -File | Measure-Object Length -Sum).Sum
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($size/1MB,2)}
} | Sort-Object SizeMB -Descending | Format-Table -AutoSize
# 6) 统计 node_modules 下最大的包
Get-ChildItem -Path ".\release\temp_asar_extract\node_modules" -Directory |
ForEach-Object {
$size=(Get-ChildItem $_.FullName -Recurse -File | Measure-Object Length -Sum).Sum
[PSCustomObject]@{Name=$_.Name; SizeMB=[math]::Round($size/1MB,2)}
} | Sort-Object SizeMB -Descending | Select-Object -First 20 | Format-Table -AutoSize2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
