Webpack 的热更新
约 2404 字大约 8 分钟
2026-04-12
Webpack 的热更新底层是如何实现的?它如何在不刷新浏览器的前提下更新页面?
考察价值: 覆盖前端工程化核心原理、WebSocket 通信、模块依赖图、虚拟 DOM / 组件更新、源码级细节,是字节 / 大厂前端工程化面试的「第一守门员」,区分度极高。
核心考点分层
初级:只说「用 WebSocket 通信,替换修改的模块」
中级:补充「HMR 的完整流程(watch 监听→编译增量 chunk→WebSocket 推送 hash→客户端请求 manifest→对比 hash→请求增量 chunk→替换模块)、HMR Runtime 的作用」
高级:讲「源码实现(webpack-dev-server 的 watch、webpack 的 HotModuleReplacementPlugin、客户端的 module.hot.accept API)、虚拟 DOM / 组件库的 HMR 适配(React Fast Refresh、Vue Hot Reload API)、边界场景(样式更新、JS 更新失败降级刷新)」
一、完整链路
Webpack热更新(Hot Module Replacement, HMR)不是简单的「WebSocket推送修改后的文件」,而是四层协同的完整链路:
- Webpack编译层:监听文件变化,增量构建修改的最小模块单元,生成「增量chunk」和「模块更新清单(manifest)」
- WebSocket通信层:webpack-dev-server(WDS)/webpack-dev-middleware(WDM)与浏览器建立长连接,同步「最新构建hash」
- 客户端HMR Runtime层:对比新旧hash,请求增量chunk,安全替换修改的模块,触发模块的
accept回调 - 框架层适配:Vue/React等框架通过
accept回调,实现组件级热重载,保留应用状态
核心目标是:只替换修改的最小单元,不刷新浏览器,不丢失当前应用的状态(如表单输入、滚动位置、组件内部state)。
二、四层协同全链路流程(按执行顺序,中级必掌握)
假设我们修改了Vue组件App.vue,完整的HMR流程如下:
阶段1:Webpack编译层 - 监听变化 + 增量构建
启动时的准备:
- Webpack启动时,会创建「模块依赖图(Module Graph)」,记录所有模块的依赖关系
- 启用
HotModuleReplacementPlugin插件,注入「HMR Runtime代码」到最终的bundle中 - WDS/WDM启动WebSocket服务器,监听指定端口
文件变化触发:
- Webpack通过
watch机制(Node.js的fs.watch/fs.watchFile,或Webpack5的chokidar优化版)监听文件系统 - 当
App.vue被修改时,Webpack会重新编译该模块及其依赖的最小模块链(增量构建,而非全量构建,大幅提升速度)
- Webpack通过
生成增量产物:
- 生成「模块更新清单(manifest.json)」:记录本次修改的模块ID、对应的增量chunk文件名、最新的构建hash
- 生成「增量chunk文件」:只包含修改的模块及其依赖的最小单元,文件名格式为
[oldhash].[moduleid].hot-update.js
阶段2:WebSocket通信层 - 同步hash
- WDS/WDM推送hash:
- 增量构建完成后,WDS/WDM通过WebSocket向所有连接的浏览器推送「最新构建hash(newHash)」
- 同时推送「模块更新事件(hot)」,通知浏览器有模块更新
阶段3:客户端HMR Runtime层 - 对比hash + 请求增量 + 安全替换
这是HMR的核心环节,由注入到bundle中的HMR Runtime代码执行:
对比新旧hash:
- 浏览器收到
newHash后,对比本地存储的「上次构建hash(oldHash)」 - 如果hash不同,说明有模块更新,进入下一步
- 浏览器收到
请求模块更新清单:
- 浏览器通过AJAX请求
[oldHash].hot-update.json(模块更新清单) - 清单中包含本次修改的模块ID列表和对应的增量chunk文件名
- 浏览器通过AJAX请求
请求增量chunk:
- 浏览器通过JSONP(避免跨域)请求所有的增量chunk文件
[oldHash].[moduleid].hot-update.js - 增量chunk文件加载完成后,会自动执行,把修改的模块注入到HMR Runtime的「模块缓存(module cache)」中
- 浏览器通过JSONP(避免跨域)请求所有的增量chunk文件
安全替换模块:
- HMR Runtime会遍历修改的模块ID列表,检查每个模块是否有「父模块的accept回调」
- 核心规则:如果某个模块的父模块(或自身)注册了
accept回调,说明该模块可以被安全替换;如果没有,会向上冒泡到入口模块,触发「降级刷新(full reload)」 - 找到可接受的父模块后,HMR Runtime会:
- 从模块缓存中删除旧模块
- 执行新模块的代码
- 触发父模块的
accept回调
阶段4:框架层适配 - 组件级热重载 + 保留状态
这是HMR能「保留应用状态」的关键,由Vue/React等框架的HMR插件实现:
Vue的适配(vue-loader + HotModuleReplacementPlugin):
- vue-loader在编译Vue组件时,会自动给每个组件的父模块(通常是入口模块或路由组件)注册
accept回调 - 当Vue组件被修改时,
accept回调会:- 保留组件的
props、state、$refs等状态 - 重新渲染组件的虚拟DOM
- 对比新旧虚拟DOM,只更新变化的DOM节点
- 保留组件的事件监听器、生命周期钩子(除了
beforeCreate/created/beforeMount/mounted,这些会重新执行)
- 保留组件的
- vue-loader在编译Vue组件时,会自动给每个组件的父模块(通常是入口模块或路由组件)注册
React的适配(React Fast Refresh,RFR):
- RFR是React官方的HMR适配方案,比旧的
react-hot-loader更稳定、更高效 - 当React函数组件被修改时,RFR会:
- 保留组件的
useState、useReducer、useRef等Hook状态 - 重新执行组件的函数体
- 对比新旧React元素,只更新变化的DOM节点
- 保留组件的事件监听器
- 保留组件的
- 当React类组件被修改时,RFR会降级刷新整个组件树(因为类组件的状态管理更复杂,难以安全保留)
- RFR是React官方的HMR适配方案,比旧的
三、源码级关键实现(核心加分项,高级必掌握)
1. Webpack编译层的关键插件:HotModuleReplacementPlugin
HotModuleReplacementPlugin是HMR的核心插件,主要做两件事:
- 注入HMR Runtime代码:把HMR Runtime的代码(如
module.hot对象、accept/decline/dispose方法、模块缓存管理)注入到最终的bundle中 - 生成增量产物:监听编译完成事件,生成「模块更新清单」和「增量chunk文件」
2. 客户端HMR Runtime的核心对象:module.hot
module.hot是HMR Runtime注入到每个模块中的对象,提供了三个核心方法:
accept(dependencies, callback):注册模块的接受回调,dependencies是需要接受更新的模块ID列表(可以是自身,也可以是子模块),callback是更新完成后执行的回调decline(dependencies):注册模块的拒绝回调,dependencies是需要拒绝更新的模块ID列表,拒绝后会向上冒泡到入口模块,触发降级刷新dispose(callback):注册模块的清理回调,callback是模块被替换前执行的回调,用于清理模块的副作用(如定时器、事件监听器、WebSocket连接)
3. Webpack5的核心优化:持久化缓存 + 增量构建优化
Webpack5对HMR做了大幅优化,主要有两点:
- 持久化缓存:把模块依赖图、编译结果等缓存到磁盘中,下次启动时直接读取缓存,大幅提升启动速度和增量构建速度
- 增量构建优化:优化了
watch机制(用chokidar替代了Node.js的原生fs.watch/fs.watchFile),优化了模块依赖图的更新逻辑,只重新编译修改的模块及其依赖的最小模块链,增量构建速度提升了50%以上
四、边界场景与降级策略(体现工程落地能力)
1. 边界场景
- 样式更新:样式模块(CSS/SCSS/Less)的HMR不需要框架层适配,HMR Runtime会直接替换样式标签,无需刷新浏览器
- JS更新失败:如果某个模块的父模块没有注册
accept回调,会向上冒泡到入口模块,触发降级刷新 - 模块有副作用:如果某个模块有未清理的副作用(如定时器、事件监听器),会导致内存泄漏或状态异常,需要用
module.hot.dispose清理 - 多模块同时更新:HMR会一次性处理所有修改的模块,只触发一次
accept回调
2. 降级策略
- 样式更新失败:降级刷新浏览器
- JS更新失败:降级刷新浏览器
- WebSocket连接断开:WDS/WDM会自动重连,重连成功后会同步最新的构建hash
五、性能优化细节(体现性能优化能力)
1. Webpack编译层的优化
- 启用持久化缓存:Webpack5中配置
cache: { type: 'filesystem' } - 优化模块解析:配置
resolve.alias、resolve.extensions、resolve.modules,减少模块解析时间 - 优化代码分割:配置
splitChunks,把公共模块(如React/Vue、lodash)分割成单独的chunk,减少增量构建的时间
2. 客户端HMR Runtime的优化
- 启用source map:配置
devtool: 'eval-cheap-module-source-map',方便调试修改后的代码 - 优化JSONP请求:配置
devServer.hotOnly: true,只启用HMR,不启用降级刷新,避免不必要的刷新
六、最终总结
Webpack热更新是四层协同的完整链路,核心是「只替换修改的最小模块单元,不刷新浏览器,保留应用状态」:
- 编译层:监听变化,增量构建,生成增量产物
- 通信层:WebSocket同步hash
- Runtime层:对比hash,请求增量,安全替换
- 框架层:组件级热重载,保留状态
Webpack5对HMR做了大幅优化,持久化缓存和增量构建优化让HMR的速度提升了数倍;开发中应注意用module.hot.dispose清理模块的副作用,避免内存泄漏或状态异常。
以上就是我对这个问题的完整理解。