dev server 每重启一次,插件就多留一份监听——Vite 8.3.0 用 closeServer/closePreviewServer 两个 hook 收尾,还告诉你这次是重启还是关停
写一个 Vite 插件,最容易被忽略的不是 transform 写错,是它什么时候该把占用的东西还回去。dev server 一天重启几十次,插件里挂的 fs.watch、定时器、子进程、websocket 客户端不会自己消失,下一次启动又挂一份,内存和句柄一路往上爬。Vite 8.3.0(2026-09-10)补上了这件事缺的最后一块:两个 teardown hook —— closeServer 和 closePreviewServer,而且 closeServer 会告诉你这次到底是重启还是真关停。
插件一直没有「下线」这个动作
Vite 插件的生命周期里早就有 build 相关的收尾 hook(buildEnd、closeBundle),但那是给打包管线用的。dev server 和 preview server 的关闭——尤其是 server.restart() 触发的重启——在 8.3.0 之前没有任何 hook 通知插件。结果就是一个反直觉的现状:
- 你以为
closeBundle会兜底,但 dev 模式下它不一定按你期待的顺序跑; - 你在
configResolved或configureServer里起的 watcher,server 关掉后没人叫停; vite --force或改vite.config.ts触发的自动重启,插件实例是新的,旧实例的资源还活着。
一个跑了半天的开发机,lsof -p <pid> | wc -l 比刚启动时多几百个描述符,通常就是这里漏出去的。
两个 hook,一个 boolean
Vite 8.3.0 的 changelog 里这行很短:
- add closeServer and closePreviewServer hooks (#23110) (e17d2d5)
用法也直白,插件对象上多两个方法:
export default function myPlugin() {
let watcher
let timer
return {
name: 'my-plugin',
configureServer(server) {
watcher = fs.watch('src', { recursive: true }, onFileChange)
timer = setInterval(pollSomething, 5000)
},
// dev server 关闭时回调,server 是关掉的那个 ViteDevServer 实例
closeServer(server) {
watcher?.close()
clearInterval(timer)
// server 上可以判断这次是不是 restart
if (server.config.command === 'serve' && restarting(server)) {
// 重启:只清状态,不用通知外部
} else {
// 真关停:可以顺手把子进程、通知、socket 一起收掉
}
},
// preview server(vite preview)关闭时回调
closePreviewServer(previewServer) {
/* 同样的收尾逻辑 */
},
}
}
关键差异在重启和关停要分开处理:
- 真关停(Ctrl+C、进程退出):可以把所有外部资源都释放掉——关掉你连的 devtools 通道、kill 子进程、取消 HTTP 长连接。
- 重启(改配置、
server.restart()):进程还在,你要保留那些本该跨重启存活的缓存(比如编译好的 AST、代理连接池),只清掉和旧 server 实例绑定的监听。
以前这两件事分不开,插件作者只能「一刀切全清」或者「干脆不清」,前者拖慢每次重启,后者就是资源泄漏。closeServer 把判断权交回给插件。
顺手修掉的那半个 bug
8.3.1(2026-09-24)跟了一个很小的修复:
- server: avoid reinitializing watcher when adding file after server close (#23572) (6f831f9)
意思是 server 已经关掉之后,如果还有文件被加进来,不要再把 watcher 初始化一遍。这条修复和上面两个 hook 是一套的——有了「下线」时机,才谈得上「下线之后别复活」。如果你的项目里写过在 server.close() 后仍然收到文件变更事件的诡异逻辑,多半踩的就是这里。
谁最需要升级
不是所有插件都要改。判断标准很简单——它在 dev 期间占用了会被重复占用的资源吗:
- 在
configureServer/configurePreviewServer里起了fs.watch、chokidar、setInterval; - 管理子进程(比如拉一个本地 mock 服务、跑一个 headless 浏览器);
- 建了长连接(websocket、SSE、devtools 桥)。
这三类插件,现在可以在 closeServer 里做确定性收尾,重启场景保留状态、关停场景彻底释放,不用再靠 process.on('exit') 这种兜底玄学。
下一步动作:在你的 node_modules 里 grep 一下常用插件有没有用上这两个 hook —— grep -rn "closePreviewServer|closeServer" node_modules/*/dist。如果某个自研或第三方插件在 configureServer 里开了 watcher 却没有对应收尾,给它提个 PR 加 closeServer,通常十几行就够;升级到 Vite 8.3.0+ 后,用 lsof -p <vite-pid> | wc -l 对比重启前后的句柄数,能直接量出这条修补省下来的资源。
评论区
登录后可评论。