2026 年 3 月,Vite 8 正式发布。这是 Vite 自 2.0 以来最重要的一次架构变革——底层构建引擎从 esbuild + Rollup 的双引擎架构,统一为基于 Rust 的 Rolldown 单一引擎。这个变化不仅带来了 10-30 倍的构建速度提升,也让我们重新审视 Electron 生态中一个被广泛使用的工具:electron-vite。
我们刚刚在一个生产项目中完成了从 electron-vite 到原生 Vite 8 的迁移。结论是:对于大多数 Electron 项目,electron-vite 所解决的问题,如今用 ~150 行标准 Vite 配置就能覆盖。更意外的是,迁移后渲染进程的打包体积几乎减半。
本文记录这次迁移的技术动机、实操过程和实际收益,希望能为有类似考量的团队提供参考。
electron-vite 解决了什么问题
Electron 应用天然包含三个独立的运行上下文:
| 进程 | 运行环境 | 典型职责 |
|---|---|---|
| Main | Node.js | 系统集成、窗口管理、IPC |
| Preload | 沙箱化 Node.js | Main 与 Renderer 的桥接层 |
| Renderer | Chromium | UI 渲染、用户交互 |
三个进程需要不同的构建目标(Node vs. 浏览器)、不同的模块格式(CJS vs. ESM)、不同的外部化策略。如果手动为每个进程分别配置 Vite,需要处理的细节不少:Node 内置模块的外部化、Electron 模块的排除、开发模式下的热重载与进程重启协调……
electron-vite 正是为此而生。它在一个 electron.vite.config.ts 中统一管理三个进程的构建配置,提供 electron-vite dev / build / preview 三条命令,自动注入合理的默认值。这在 Vite 5/6/7 时代是很有价值的——你不需要理解 Rollup 的 external 回调怎么写,不需要手动协调三个 watcher 的生命周期。
但代价是什么
electron-vite 官方列出了 11 项功能特性。我们在项目中做了一次逐项审计,发现实际使用的只有 6 项:
| 功能 | 是否使用 | 说明 |
|---|---|---|
| 三进程分别构建 | ✅ | 核心功能 |
| Main/Preload 热重载 | ✅ | 文件变更时重启 Electron |
| Renderer HMR | ✅ | 标准 Vite 开发服务器 |
externalizeDeps | ✅ | Node 目标不打包 npm 依赖 |
?asset 导入查询 | ✅ | 图标路径解析(3 处调用) |
| ESM → CJS 输出 | ✅ | Electron 主进程需要 CJS |
| V8 字节码保护 | ❌ | |
| 字符串混淆 | ❌ | |
| SWC 装饰器元数据 | ❌ | |
| 进程级环境变量前缀 | ❌ | |
?nodeWorker 等特殊查询 | ❌ |
5 项未使用的功能作为死重量一直跟随项目。更关键的是,electron-vite 还带来了一系列隐性成本:
版本锁定。electron-vite 通过 peerDependency 控制 Vite 版本。当 Vite 8 发布时,electron-vite@5 的 peer 声明还停留在 "^5 || ^6 || ^7",直接阻断了升级路径。你的项目对 Vite 版本的控制权,实际上交给了一个第三方包的发布节奏。
依赖膨胀。electron-vite 通常与 @electron-toolkit/* 系列包一起使用。其中 @electron-toolkit/utils 是作为生产依赖打入最终应用的——它会出现在用户下载的安装包里。而它提供的三个工具函数,本质上是对 Electron 原生 API 的一层转发:
is.dev → !app.isPackaged // 一行代码
electronApp.setAppUserModelId() → app.setAppUserModelId() // 一行代码
optimizer.watchWindowShortcuts() → 约 10 行 Electron API 调用
@electron-toolkit/tsconfig 和两个 ESLint 配置包也是类似的薄封装——分别转发了 typescript-eslint 和 eslint-plugin-prettier,除了增加一层版本耦合,没有提供额外的 API 价值。
Vite 8 改变了什么
Vite 8 带来的不只是版本号的递增,而是底层架构的根本性重构。理解这些变化,是判断 electron-vite 是否还有必要的前提。
Rolldown:统一的 Rust 构建引擎
Vite 此前使用两套引擎:esbuild 负责开发阶段的依赖预打包和 TypeScript/JSX 转译(追求速度),Rollup 负责生产构建(追求优化质量和插件生态)。双引擎意味着两套转换管线、两套插件体系,以及大量用于对齐两者行为的胶水代码。
Vite 8 用 Rolldown(VoidZero 团队基于 Rust 构建的打包器)替代了两者。Rolldown 兼容 Rollup 的插件 API,大多数现有 Vite 插件无需修改即可运行。根据官方公布的实际案例:
- Linear:生产构建从 46s 降至 6s
- Ramp:构建时间减少 57%
- Mercedes-Benz.io:构建时间减少 38%
Oxc:更快的转译与压缩
Vite 8 用 Oxc(同样基于 Rust)替代了 esbuild 在 JavaScript 转译和压缩中的角色。@vitejs/plugin-react v6 也随之发布,使用 Oxc 进行 React Refresh 转换,不再依赖 Babel,安装体积更小。
对于我们的场景,Oxc 压缩器与 Rolldown 的 tree-shaking 协同工作,产生了比旧方案显著更紧凑的输出——具体数据见下文。
Environment API(Release Candidate)
Vite 6 引入、Vite 8 继续完善的 Environment API,允许在单个 vite.config.ts 中定义多个构建环境(client、ssr、worker……),每个环境拥有独立的构建目标和模块图。
这与 electron-vite 用一个配置文件管理三个进程的思路异曲同工——只不过 Environment API 是 Vite 原生的、标准化的方案。虽然目前该 API 尚处于 Release Candidate 阶段,还没有对 lib 模式的完整支持,但它指明了方向:未来 Vite 自身就能原生表达 Electron 的多进程构建模型,而无需任何外部包装。
其他值得关注的新特性
- 内置 tsconfig
paths支持:设置resolve.tsconfigPaths: true即可启用 TypeScript 路径别名解析,无需额外插件。 - Browser console 转发:
server.forwardConsole将浏览器控制台日志转发到终端,在配合 AI 编码代理时尤其有用。 - 内置
emitDecoratorMetadata支持:不再需要外部 SWC 插件——这恰好是 electron-vite 的卖点之一。
实操:~150 行配置替换 electron-vite
以下是我们在实际项目中的迁移方案。
三个独立的 Vite 配置文件
将原来 electron-vite 的单配置文件拆分为三个标准 Vite 配置。以主进程为例:
// vite.main.config.ts
import { defineConfig } from 'vite'
import { builtinModules } from 'node:module'
const external = ['electron', ...builtinModules, ...builtinModules.map((m) => `node:${m}`)]
export default defineConfig({
build: {
outDir: 'out/main',
lib: {
entry: 'src/main/index.ts',
formats: ['cjs'],
fileName: () => 'index.js',
},
rollupOptions: {
external: (id) =>
external.some((e) => id === e || id.startsWith(`${e}/`)) || !/^[./]/.test(id),
},
minify: false,
emptyOutDir: true,
},
resolve: {
conditions: ['node'],
mainFields: ['module', 'jsnext:main', 'jsnext'],
},
})
关键在于 rollupOptions.external 这个回调函数。它替代了 electron-vite 的 externalizeDeps: true 魔法布尔值,逻辑完全透明:外部化 electron、所有 Node 内置模块、以及任何裸模块标识符(npm 包),只打包相对路径和绝对路径的导入。一行函数,显式可审计。
Preload 配置结构相同,额外加了 esbuild.drop: ['console', 'debugger'] 去除调试输出。Renderer 配置则是标准的浏览器端 Vite 配置,携带 React、styled-jsx、wyw 等插件,与之前完全一致。
三个配置文件加起来约 80 行。
一个 90 行的开发编排脚本
electron-vite dev -w 的核心行为是:启动 Renderer 开发服务器、以 watch 模式构建 Main 和 Preload、在构建完成后启动 Electron、在后续重新构建时重启 Electron。
用 Vite 的 JS API 复现这个流程,代码量出人意料地少:
// scripts/electron-dev.ts(核心逻辑,省略类型和错误处理)
import { build, createServer } from 'vite'
import { spawn } from 'node:child_process'
import electron from 'electron'
// 1. 启动 Renderer 开发服务器
const server = await createServer({ configFile: 'vite.renderer.config.ts' })
await server.listen()
const rendererUrl = server.resolvedUrls!.local[0]
let mainReady = false, preloadReady = false
function onBundleReady() {
if (mainReady && preloadReady) startElectron(rendererUrl)
}
// 2. Main 进程 — watch 模式
const mainWatcher = await build({ configFile: 'vite.main.config.ts', build: { watch: {} } })
mainWatcher.on('event', (e) => {
if (e.code === 'BUNDLE_END') { mainReady = true; onBundleReady() }
})
// 3. Preload — watch 模式
const preloadWatcher = await build({ configFile: 'vite.preload.config.ts', build: { watch: {} } })
preloadWatcher.on('event', (e) => {
if (e.code === 'BUNDLE_END') { preloadReady = true; onBundleReady() }
})
这里有一个容易忽略的细节:Electron 必须在 Main 和 Preload 都完成首次构建后才能启动。如果在 Main 构建完成后就立即 spawn Electron,而 Preload 还没就绪,会导致窗口加载失败。双标志位门控(mainReady && preloadReady)是一个必要的防护。
替换 @electron-toolkit/utils
三个工具函数的替换逻辑如下,总共约 15 行代码:
| 之前 | 之后 |
|---|---|
is.dev | !app.isPackaged |
electronApp.setAppUserModelId(id) | app.setAppUserModelId(id) |
optimizer.watchWindowShortcuts(win) | 内联 ~10 行 F12/Cmd+R 处理逻辑 |
import icon from '…/icon.png?asset' | path.join(__dirname, '../../resources/icon.png') |
这些都是 Electron 原生 API 的直接调用,不需要任何抽象层。
npm scripts 的变化
{
"dev": "tsx scripts/electron-dev.ts",
"build": "vite build --config vite.main.config.ts && vite build --config vite.preload.config.ts && vite build --config vite.renderer.config.ts"
}
三个 vite build 顺序执行。由于它们输出到不同的目录、依赖图完全独立,未来如果构建时间增长,也可以轻松改为并行执行。
迁移收益
打包体积:Renderer JS 减少 49%
这是我们没有预期到的最大收益。相同的源代码、相同的依赖,仅仅切换了构建工具链:
| 产物 | 迁移前(Vite 7 + electron-vite) | 迁移后(Vite 8 原生) | 变化 |
|---|---|---|---|
renderer/assets/index.css | 50.03 kB | 48.78 kB (gzip: 13.64 kB) | -2.5% |
renderer/assets/index.js | 1,927.25 kB | 989.94 kB (gzip: 284.35 kB) | -48.6% |
CSS 的变化可以忽略,但 JS 从近 2 MB 缩减到不足 1 MB,减少了将近一半。
这个巨大的差异主要来自 Rolldown 相比 Rollup 在以下方面的改进:
- 统一管线的 tree-shaking。Rolldown 在同一个 Rust 管线中完成解析、转换、tree-shaking 和压缩,不需要在多个工具间序列化/反序列化 AST,可以做出更精准的死代码消除判断。
- Oxc 压缩器的优化。Oxc 替代 esbuild 作为 JS 压缩器,在作用域分析和标识符重命名上有更积极的策略。
- 更好的 CJS 互操作。Vite 8 统一了 CommonJS 模块的默认导入处理方式,减少了因为兼容性垫片导致的冗余代码。
对于 Electron 应用来说,Renderer 的 JS 体积直接影响窗口的首次加载速度。近半的体积缩减,在用户体验上是实实在在的提升。
依赖数量:净减少 34 个包
| 指标 | 数据 |
|---|---|
| 移除的直接依赖 | 5 个(electron-vite + 4 个 @electron-toolkit/*) |
| 新增的直接依赖 | 2 个(typescript-eslint + eslint-plugin-prettier,此前是隐式传递依赖) |
| 移除的传递依赖 | 54 个 |
| 新增的传递依赖 | 20 个 |
| 净减少 | 34 个包 |
更少的依赖意味着更快的 npm install、更小的攻击面、更少的版本冲突。其中 @electron-toolkit/utils 从生产依赖中移除,意味着最终打包给用户的应用也少了一个第三方包。
升级自由度:不再被阻断
现在项目直接依赖 vite@^8.0.0,不再经过任何中间层。未来 Vite 9 发布时,升级只需要 npm install vite@latest 并查阅官方迁移指南,不需要等待 electron-vite 跟进。
构建行为完全透明
三个配置文件都是标准 Vite 配置,任何 Vite 文档、插件文档或社区答案都直接适用。externalizeDeps 行为从一个布尔值变成了一行可读的函数;?asset 转换从一个不透明的查询字符串约定变成了一行 path.join。排查构建问题时,不再需要先理解 electron-vite 的内部实现。
那 electron-vite 还有价值吗
有。这篇文章的结论不是「electron-vite 不好」,而是**「在 Vite 8 之后,它的不可替代性大幅降低了」**。
electron-vite 仍然在以下场景中有独特的价值:
- V8 字节码保护:如果你需要将源代码编译为 V8 bytecode 以防止反编译,这是 electron-vite 最独特的功能之一,原生 Vite 无法提供。
- 零配置快速启动:对于新项目或 Electron 初学者,
npm create @quick-start/electron提供的脚手架体验仍然是目前最顺滑的 Electron + Vite 起步方式。 - 多框架模板:Vue、React、Svelte、SolidJS 的即开即用模板,降低了入门门槛。
但如果你的项目:
- ✅ 已经有成熟的构建配置和开发流程
- ✅ 不需要 V8 字节码保护
- ✅ 想要第一时间使用最新版 Vite
- ✅ 追求最小的依赖面和最大的构建透明度
那么在 Vite 8 的时代,三个标准配置文件加一个简短的开发脚本,就是 electron-vite 的完整替代方案。
展望:Environment API 的成熟
Vite 的 Environment API 目前处于 Release Candidate 阶段。一旦它稳定并完善对 lib 模式的支持,我们有可能将三个独立的配置文件合回一个 vite.config.ts——用 Vite 原生的多环境支持来表达 Electron 的 Main / Preload / Renderer 三进程模型:
// 未来的可能形态(API 尚未稳定,仅为概念示意)
export default defineConfig({
environments: {
main: {
build: { outDir: 'out/main', lib: { entry: 'src/main/index.ts', formats: ['cjs'] } },
resolve: { conditions: ['node'] },
},
preload: {
build: { outDir: 'out/preload', lib: { entry: 'src/preload/index.ts', formats: ['cjs'] } },
resolve: { conditions: ['node'] },
},
// renderer 即默认的 client 环境
},
})
当这一天到来时,Vite 自身就是最好的「Electron 构建工具」——不需要任何前缀、后缀或包装。
本文基于一个实际生产项目的迁移经验撰写。该项目是一个基于 Electron 40 + React 19 + TypeScript 的桌面应用。