HMR热更新慢?我在项目中优化Webpack和Vite的实战经验
HMR改完代码不刷新?我折腾了大半天才搞明白是 Webpack 模块缓存没清干净
今天下午三点,我正改一个组件的 props 类型校验,改完保存——页面纹丝不动。F5 刷新一下?行。但 HMR 就是不触发。我又加了个 console.log,保存,没反应。再加个 div 文字,还是没反应。Ctrl+Shift+R 强刷都懒得按了,直接关掉 dev server 重启……结果一启动,热更新又好了。等我再改两行,又卡住。这已经不是第一次了,但这次我决定不惯着它,必须挖到底。
先说结论:根本原因不是 React Fast Refresh 配置问题,也不是 webpack-dev-server 版本 bug,而是我本地用了 webpack-plugin-serve(对,就是那个轻量替代品),它默认开启了 moduleCache,而且这个 cache 在 HMR 过程中不会自动失效——哪怕你改了源文件,它还拿着上一次 resolve 出来的 module 对象,导致 diff 结果为空,HMR 就默默跳过了。
这里我踩了个坑:一开始以为是 react-refresh-webpack-plugin 没配对,翻文档、重装插件、降级到 v0.5.10,甚至把 babel-loader 的 cacheDirectory 关了,全都没用。后来在 Chrome DevTools 的 Sources 面板里,点开 webpack:// 下某个组件,右键 “Reveal in sidebar”,发现文件时间戳确实是新的,但内容却和旧的一模一样!我当场愣住——这说明 webpack 根本没重新编译这个模块。
于是我在 webpack config 里加了 stats: 'verbose',跑起来后改文件,看 terminal 输出:果然,compiling... 后面压根没出现那个组件的路径,只有几个 utils 和入口文件。这下基本确定是模块解析层被缓存了。
查了一圈,发现 webpack-plugin-serve 的文档里有一行小字:moduleCache: true 是默认开启的,并且“适用于大多数静态开发场景”。哈,我们这种高频改组件 + 多层嵌套 + 动态 import 的项目,显然不属于“大多数静态开发场景”。
后来试了下发现,只要把 moduleCache 关掉,问题立马解决。但关了之后性能会掉一点(首次 HMR 稍慢 200ms 左右),不过比起手动重启 dev server 的 8 秒等待,这点延迟完全可以接受。
核心代码就这几行
我的 webpack.dev.js 里原本长这样:
const { ServePlugin } = require('webpack-plugin-serve');
module.exports = {
plugins: [
new ServePlugin({
port: 3000,
open: true,
// 其他配置...
})
]
};
改成这样就 OK 了:
const { ServePlugin } = require('webpack-plugin-serve');
module.exports = {
plugins: [
new ServePlugin({
port: 3000,
open: true,
// 👇 关键:显式禁用 moduleCache
moduleCache: false,
// 👇 顺手加上这个,避免 HMR 时因为 resolve 失败 fallback 到缓存
hot: {
overlay: true
}
})
],
// 👇 再保险一点:强制每次 HMR 前清空 resolver 缓存
resolve: {
alias: {
// 如果你有 alias,这里不用动
}
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
};
顺便提一句:如果你用的是 webpack-dev-server(不是 plugin 版),那大概率不会遇到这个问题——它的默认行为是每次变更都会走完整的 module graph 构建,缓存策略更激进但也更可靠。但如果你像我一样为了省点内存、少装个包而选了 webpack-plugin-serve,就得手动管好这个 moduleCache 开关。
另外,我还顺手排查了几个常见干扰项,列出来省得你再踩一遍:
- React.memo 或 shouldComponentUpdate 搞鬼? 不是。HMR 失效时连最外层 App 组件都不更新,跟业务逻辑完全无关。
- tsconfig.json 里
incremental: true影响 HMR? 不影响。tsc 只负责类型检查,webpack 的 ts-loader 或 babel-loader 才是真正干活的。 - node_modules 被 watch 忽略了? 我确实在 watchOptions 里加了
ignored: /node_modules/,但这只影响文件系统监听,不影响模块解析缓存。 - VS Code 的文件保存延迟? 关了“Delay until buffer is saved”选项后依然复现,排除。
还有一个小尾巴:改完后,偶尔还会出现某次修改没触发 HMR,但第二次保存就正常了。我试了几次,发现基本都是在快速连续保存(比如 Ctrl+S 连按两次)时发生。推测是 plugin 的文件监听 debounce 时间(默认 300ms)和模块缓存清理时机没对齐。不过这个概率很低,我也没深究——毕竟比以前每改三行就要重启一次强多了。
最后多聊一句原理:Webpack 的 HMR 流程本质是“diff → accept → update”。而 moduleCache: true 会让 resolver 直接返回上次缓存的 Module 实例,导致 NormalModuleFactory.create 返回的 module 对象和上次完全一致,diff 阶段就判定“无变化”,后续流程直接跳过。你看到的“没反应”,其实是 webpack 自己决定不反应。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案,比如怎么在开启 moduleCache 的前提下精准失效特定模块,欢迎评论区交流。我回头也打算给 webpack-plugin-serve 提个 PR,至少把这个坑写进 FAQ 里。

暂无评论