Vite 8 之后,你可能不再需要 electron-vite

17 阅读11分钟

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 应用天然包含三个独立的运行上下文:

进程运行环境典型职责
MainNode.js系统集成、窗口管理、IPC
Preload沙箱化 Node.jsMain 与 Renderer 的桥接层
RendererChromiumUI 渲染、用户交互

三个进程需要不同的构建目标(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 开发服务器
externalizeDepsNode 目标不打包 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-eslinteslint-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 中定义多个构建环境(clientssrworker……),每个环境拥有独立的构建目标和模块图。

这与 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.css50.03 kB48.78 kB (gzip: 13.64 kB)-2.5%
renderer/assets/index.js1,927.25 kB989.94 kB (gzip: 284.35 kB)-48.6%

CSS 的变化可以忽略,但 JS 从近 2 MB 缩减到不足 1 MB,减少了将近一半。

这个巨大的差异主要来自 Rolldown 相比 Rollup 在以下方面的改进:

  1. 统一管线的 tree-shaking。Rolldown 在同一个 Rust 管线中完成解析、转换、tree-shaking 和压缩,不需要在多个工具间序列化/反序列化 AST,可以做出更精准的死代码消除判断。
  2. Oxc 压缩器的优化。Oxc 替代 esbuild 作为 JS 压缩器,在作用域分析和标识符重命名上有更积极的策略。
  3. 更好的 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 的桌面应用。