我们有一个桌面应用的项目技术栈是 Tauri + Vue3 + Vite 8。
每次 dev 冷启动,终端已经打出 Running app.exe 了,窗口还要再等一分多钟才出来,慢的时候将近 100 秒。
但进程一旦起来,改代码 HMR 又很快,一两秒就刷新。
开发体验很差,然后决定排查一下原因看能不能优化一下
首先确定时间花在哪?
先在前端埋两个点,用 @tauri-apps/plugin-log 输出到日志文件
ts
perfLog(`main.ts imports evaluated perfNow=${Math.round(performance.now())}ms`) // 入口模块求值完
perfLog(`window.show perfNow=${Math.round(performance.now())}ms`) // 真正 show 窗口
perfNow 是 performance.now(),从 WebView 导航开始算。跑出来:
ini
window.show perfNow=92548ms
92 秒,而 mount 本身只有 20 多毫秒。时间没花在业务代码上,是花在把入口模块图加载完之前。
是前端还是服务端慢?
用 curl 直接打 Vite,带上 DEBUG=vite:time 看每个请求的耗时。冷启动按顺序打几个端点
bash
/@vite/client 28.8s
/src/main.ts 0.9ms
/src/styles/index.scss 38.0s
/src/styles/tailwind.css 0.26ms
@vite/client 是个静态文件,不用编译,也要 28 秒;index.scss 里就一行 @use,也 38 秒。
每个新进程启动都有一笔十几到几十秒的一次性开销,付完之前的请求全卡着,付完之后同进程就都快了。冷启动慢、HMR 快就是这么来的,HMR 是同一个进程,这钱早付过了。
curl 不走 WebView 也能复现,所以是 Vite 服务端的问题,跟 Tauri、WebView2 无关。
走了个弯路-怀疑杀软
「每个进程第一次很慢、之后很快、数值还忽高忽低」,这表现挺像杀毒软件实时扫描的。我去 Defender 加了项目目录排除,后来干脆把实时保护关了重测。
没用,还是 70 秒。
CPU profiler
没头绪了,AI 提示抓个 CPU profile 看看。用 Vite 的 JS API 起服务,配 inspector 给冷的 transformRequest 采样,比 curl 干净,不受端口和 WebView 干扰:
js
const server = await createServer({ /* 项目配置 */ })
await server.listen()
await server.transformRequest('/@vite/client')
采样总共 141 秒,其中 96.4 秒(68%)压在一个没有 JS 栈的原生帧 start@:0 上。按调用路径还原一下:
scss
fs.watch (ReadDirectoryChangesW)
← createFsWatchInstance (chokidar)
← createServer 里的 chokidar.watch([root]) 递归遍历
剩下的热点是 node:path 的 resolve/join 和 chokidar 的 _isIgnored、picomatch,都是遍历目录时的开销。
Vite dev 在 createServer 的时候就用 chokidar 递归监听整个项目根,Windows 上走 fs.watch,一个目录一个目录、一个文件一个文件地建监听句柄。
而我们项目根下有个 src-tauri/target,Rust 的编译产物,好几个 G、几万个文件,也被一起递归注册了。这些句柄的注册在启动时就开始铺,server.listen() 又不等它做完,早来的请求都卡在这场遍历后面。那笔几十秒的开销就是它。
干这事的不是 ensureWatchedFile。它有个 !file.startsWith(root) 的判断,只监听 root 外面的文件,src-tauri/target 在 root 里,根本不会去加。真正触发的是启动时那句 chokidar.watch([root])。start@:0 没有 JS 栈,上面的父帧是我按代码路径推的。
问题确认了怎么修改呢?
让 Vite 别去监听这个目录:
ts
// vite.config.ts
server: {
watch: {
ignored: ['**/src-tauri/target/**', '**/build-output/**', '**/dist/**']
}
}
有个细节:.git、node_modules、.vite 缓存这些 Vite 默认就忽略了(看 resolveChokidarOptions 源码,你自己配的 ignored 是在默认值后面追加,不是替换)
同一个 transformRequest 前后对比:
| transform | 改前 | 改后 |
|---|---|---|
/@vite/client |
16550ms | 1291ms |
/src/main.ts |
33110ms | 347ms |
/src/App.vue |
44306ms | 1087ms |
/src/main.ts 在前面 curl 那次只有 0.9ms,这次却 33 秒,同一个请求差了四个数量级。慢不慢取决于它撞进那场遍历的哪一段:curl 那次正好落在空隙里,这次三个请求顺序 await,都落在遍历还没结束的窗口里,16、33、44 秒就是各自排到第几秒才轮上。遍历结束就都快了。
实际 pnpm dev 冷启动,window.show 从 70~100 秒降到 4.4 秒。
前端 HMR 和 Rust 重编都不受影响:忽略的都是构建产物,src/ 该热更新还是热更新;Rust 那边有自己的 watcher,跟 Vite 前端这套是分开的。
折腾大半天,最后就加了一行 ignored。Windows + Tauri,项目根下有 src-tauri/target 这种又多又大的目录的,dev 冷启动慢可以先往这查。