HMR热更新慢?我在项目中优化Webpack和Vite的实战经验

志亮🍀 优化 阅读 2,661
赞 25 收藏
二维码
手机扫码查看
反馈

HMR改完代码不刷新?我折腾了大半天才搞明白是 Webpack 模块缓存没清干净

今天下午三点,我正改一个组件的 props 类型校验,改完保存——页面纹丝不动。F5 刷新一下?行。但 HMR 就是不触发。我又加了个 console.log,保存,没反应。再加个 div 文字,还是没反应。Ctrl+Shift+R 强刷都懒得按了,直接关掉 dev server 重启……结果一启动,热更新又好了。等我再改两行,又卡住。这已经不是第一次了,但这次我决定不惯着它,必须挖到底。

HMR热更新慢?我在项目中优化Webpack和Vite的实战经验

先说结论:根本原因不是 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-loadercacheDirectory 关了,全都没用。后来在 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 里。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论