webpack 长缓存实战:用 optimization.runtimeChunk 拆分 Runtime Chunk,让 [chunkhash] 真正生效
发布时间:2026/9/7 10:28:15 锦皓数字建站

webpack 长缓存实战用 optimization.runtimeChunk 拆分 Runtime Chunk让 [chunkhash] 真正生效【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack在 webpack 中结合[chunkhash]与 Code Splitting 时入口 chunk 因内嵌 webpack runtime含 chunk hash 映射而每次构建都变化导致缓存失效。本文基于仓库示例 chunkhash 讲解如何用optimization.runtimeChunk将 runtime 拆分为独立 chunk 并内联进 HTML恢复各 chunk 的长期缓存能力并结合 webpack 源码RuntimeChunkPlugin、normalization.js 等拆解该选项的完整取值与生效链路帮助你既掌握可复制的配置也理解产物中 runtime chunk 的内部结构。一、问题为什么 [chunkhash] 在入口 chunk 上失效webpack 对每个 chunk 计算chunkhash时hash 依据是该 chunk 的内容包括其中所有模块以及runtime 中记录的 chunk 文件名映射表。而入口 chunk 默认包含两部分内容入口模块本身的应用代码webpack runtime其中包括 chunk 加载逻辑以及__webpack_require__.u这类chunkId → 带 hash 文件名的映射函数。问题在于只要任何一个异步 chunk 的内容发生变化runtime 里的映射表就会更新进而导致入口 chunk 的内容变化、[chunkhash]改变、文件名变化。入口 chunk 每次发版都无法命中浏览器缓存[chunkhash]对最重要的那个文件几乎失去了意义。这正是 chunkhash 示例 开头指出的核心矛盾入口 chunk 包含 webpack runtime 和 chunkhash 映射它总是被更新[chunkhash]因此形同虚设。二、解决方案把 runtime 单独拆成一个 chunk解决思路很直接创建一个只包含 webpack runtime含 chunkhash 映射的独立 chunk。这样应用代码的任何变化只会影响各自 chunk 的 hashruntime chunk 只在新增/删除 chunk这类结构变化时更新其 hash 可以长期稳定。webpack 通过optimization.runtimeChunk选项实现这一点。为了让这个很小的 chunk 不额外产生一次网络请求官方示例进一步把它内联进 HTML 页面。示例要求的核心配置有两项output.filename中使用[chunkhash]output.chunkFilename中使用[chunkhash]。说明仓库中该示例的实际配置文件写的是[name].chunkhash.js字面量见下文这是示例生成基础设施的限制——构建产物的 hash 部分由模板系统替换。README 明确提示在真实项目中你应该使用[chunkhash]占位符。2.1 示例源码入口模块 example.js 触发两个异步 chunk// some module import(./async1); import(./async2);2.2 完整 webpack 配置以下是 webpack.config.js 的完整内容逐项标注了作用use strict; const path require(path); /** type {import(webpack).Configuration} */ const config { // mode: development || production, entry: { main: ./example // 命名入口 main决定 runtime chunk 名为 runtime~main }, optimization: { runtimeChunk: true // 为每个入口创建独立 runtime chunkmultiple 语义 }, output: { path: path.join(__dirname, dist), filename: [name].chunkhash.js, // 实际项目应写作 [name].[chunkhash].js chunkFilename: [name].chunkhash.js // 异步 chunk 同样带上 hash } }; module.exports config;要点runtimeChunk: true等价于multiple即每个入口各生成一个 runtime chunk命名为runtime~入口名若多个入口希望共用一个 runtime可改为runtimeChunk: singlechunk 名固定为runtimeoutput.filename与output.chunkFilename都带上[chunkhash]入口 chunk 与异步 chunk 均可随内容变化而换名未变化的 chunk 保持缓存命中。2.3 HTML 模板内联 runtime chunkindex.html 中runtime 文件的压缩后内容被直接内联到script中只有应用入口 chunk 走外部请求html head /head body !-- inlined minimized file runtime~main.[chunkhash].js -- script {/* 此处内联压缩后的 runtime~main.[chunkhash].js 全部内容 即产物中那段自执行 IIFE见 2.4 节分析 */} /script script srcdist/main.[chunkhash].js/script /body /html在真实工程里这段内联通常由模板引擎如 html-webpack-plugin 的模板占位符在构建时注入。其收益是runtime 不再占用一次独立请求代价是 runtime 内容随 chunk 结构变化时HTML 本身会变化——但这不影响各 JS 文件的缓存。三、读懂构建产物runtime chunk 里到底有什么按上述配置构建后产物包含 4 个文件开发模式下的 stats 输出asset runtime~main.[chunkhash].js 11.7 KiB [emitted] (name: runtime~main) asset main.[chunkhash].js 813 bytes [emitted] (name: main) asset 2.[chunkhash].js 285 bytes [emitted] asset 3.[chunkhash].js 267 bytes [emitted] Entrypoint main 12.5 KiB runtime~main.[chunkhash].js 11.7 KiB main.[chunkhash].js 813 bytes chunk (runtime: runtime~main) main.[chunkhash].js (main) 55 bytes [initial] [rendered] chunk (runtime: runtime~main) runtime~main.[chunkhash].js (runtime~main) 7.41 KiB [entry] [rendered] ./example main runtime modules 7.41 KiB 10 modules chunk (runtime: runtime~main) 2.[chunkhash].js 28 bytes [rendered] chunk (runtime: runtime~main) 3.[chunkhash].js 28 bytes [rendered] webpack X.X.X compiled successfully生产模式开启压缩下 runtime chunk 压缩到 2.74 KiB异步 chunk id 变为确定性数字18、471asset runtime~main.[chunkhash].js 2.74 KiB [emitted] [minimized] (name: runtime~main) asset main.[chunkhash].js 142 bytes [emitted] [minimized] (name: main) asset 471.[chunkhash].js 66 bytes [emitted] [minimized] asset 18.[chunkhash].js 64 bytes [emitted] [minimized] Entrypoint main 2.88 KiB runtime~main.[chunkhash].js 2.74 KiB main.[chunkhash].js 142 bytes3.1 runtime~main chunk 的关键结构runtime~main 产物 是一个自执行 IIFE共含 10 个 runtime modulestats 中runtime modules 7.41 KiB 10 modules。摘出与本主题最相关的几段/******/ (() { // webpackBootstrap /******/ use strict; /******/ var __webpack_modules__ ({}); // 注意模块表为空 /******/ const __webpack_module_cache__ {};模块表__webpack_modules__是空的——没有任何真实模块代码。它只有机制没有内容。继续看 chunk 文件名映射函数这就是让入口 hash 变化的元凶现在它被隔离在这个 chunk 里/******/ /* webpack/runtime/get javascript chunk filename */ /******/ // This function allow to reference async chunks /******/ __webpack_require__.u (chunkId) (chunkId .[chunkhash].js);以及 JSONP 加载机制中的installedChunks状态表。注意运行时installedChunks只记录了入口 chunkid 为 1为已加载runtime chunk 自身id 0不在其中/******/ /* webpack/runtime/jsonp chunk loading */ /******/ (() { /******/ // undefined chunk not loaded, null chunk preloaded/prefetched /******/ // [resolve, reject, Promise] chunk loading, 0 chunk loaded /******/ const installedChunks { /******/ 1: 0 /******/ }; /******/ __webpack_require__.f.j (chunkId, promises) { /* JSONP 加载逻辑 */ }; /******/ __webpack_require__.O.j (chunkId) (installedChunks[chunkId] 0); /******/ const webpackJsonpCallback (parentChunkLoadingFunction, data) { /******/ let [chunkIds, moreModules, runtime] data; /******/ /* 将 moreModules 合入 __webpack_modules__标记 chunkIds 为已加载 /******/ 执行附带的 runtime并触发 __webpack_require__.O 的 deferred 回调 */ /******/ } /******/ const chunkLoadingGlobal self[webpackChunk] self[webpackChunk] || []; /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); /******/ chunkLoadingGlobal.push webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); /******/ })();其余 runtime module__webpack_require__.e异步 ensure、__webpack_require__.l注入 script 标签、__webpack_require__.t创建 fake namespace、__webpack_require__.d定义 getter、__webpack_require__.r标记__esModule、__webpack_require__.p dist/等在完整产物 dist/runtime~main 一节中均有原始注释可对照阅读。3.2 main chunk 变得极其干净main 产物 只剩入口模块代码通过全局webpackChunk数组把模块 push 进 runtime 已建好的加载机制(self[webpackChunk] self[webpackChunk] || []).push([[0],[ /* 0 */ /*!********************!*\ !*** ./example.js ***! \********************/ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { // some module __webpack_require__.e(/*! import() */ 2).then(() (__webpack_require__.t(/*! ./async1 */ 1, 23))); __webpack_require__.e(/*! import() */ 3).then(() (__webpack_require__.t(/*! ./async2 */ 2, 23))); /***/ }) ], /******/ __webpack_require__ { // webpackRuntimeModules /******/ var __webpack_exec__ (moduleId) (__webpack_require__(moduleId)) /******/ var __webpack_exports__ (__webpack_exec__(0)); /******/ } ]);可以看到 main chunk 里不再有任何加载机制代码。__webpack_require__.e(2)、e(3)调用由 runtime chunk 提供的机制完成随后经__webpack_require__.t(module, 23)创建 namespace。这意味着 main chunk 的[chunkhash]只取决于example.js自身与异步 chunk id 的分配二者稳定时缓存即可长期命中。四、源码深潜optimization.runtimeChunk 的取值与生效链路4.1 默认值与规范化从 defaults.js 可以看到该选项默认关闭D(optimization, runtimeChunk, false)。各取值的规范化逻辑集中在 normalization.js 的getNormalizedOptimizationRuntimeChunk中用户配置规范化结果语义false默认false不拆分runtime 留在入口 chunksingle{ name: () runtime }所有入口共用一个名为runtime的 runtime chunktrue/multiple{ name: (entrypoint) \runtime~${entrypoint.name} }| 每个入口一个 runtime chunk如runtime~main{ name: xxx }{ name: () xxx }自定义固定名称{ name: (ep) ... }原样保留函数按入口动态命名4.2 插件注册与命名配置生效入口在 WebpackOptionsApply.js只要options.optimization.runtimeChunk为真值就创建RuntimeChunkPlugin并apply(compiler)。RuntimeChunkPlugin 的实现只有 30 行左右其核心是监听compilation.hooks.addEntryRuntimeChunkPlugin.jsapply(compiler) { compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) { compilation.hooks.addEntry.tap(PLUGIN_NAME, (_, { name: entryName }) { if (entryName undefined) return; const data compilation.entries.get(entryName); if (data.options.runtime undefined !data.options.dependOn) { // Determine runtime chunk name let name this.options.name; if (typeof name function) { name name({ name: entryName }); } data.options.runtime name; } }); }); }几个从源码可直接确认的事实默认命名规则构造函数中name缺省为(entrypoint) \runtime~${entrypoint.name}[RuntimeChunkPlugin.js](https://link.gitcode.com/i/b98e0241732aeaadc85109e1aea88a1f#L20-L26)产物名runtime~main 由此而来生效条件仅当入口未显式设置runtime选项且不是dependOn依赖型入口时插件才介入设置data.options.runtime——即entry: { name: { runtime: ... } }的手动指定优先级更高dependOn场景下 runtime 归属由主入口决定无匿名入口entryName undefined时直接跳过说明runtimeChunk与无名字入口不配合。随后Entrypoint 通过setRuntimeChunk/getRuntimeChunk维护入口点与 runtime chunk 的绑定当存在 HTML 入口时HtmlEntryDependency 会保证 runtime chunk 的 script 被排在入口 chunk 之前注入源码注释明确提到 runtime chunk first (whenoptimization.runtimeChunksplits it off)——这与手工内联 HTML 中runtime 脚本在前、main 脚本在后的顺序要求完全一致。五、实践建议与相关示例缓存粒度权衡拆分 runtime 后[chunkhash]的失效范围从任何模块变化都改变入口收窄到chunk 集合/结构变化才改变 runtime chunk。日常业务迭代中绝大多数发布未改动的 chunk 都能稳定命中缓存。runtime 内联 vs 独立请求runtime chunk压缩后本例约 2.74 KiB内容变化频率低、体积小内联进 HTML 可省掉一次请求并避免它自身的缓存问题若用独立script引用它同样带[chunkhash]也能长期缓存。两种做法在 chunkhash 示例 中都有体现模板注释inlined minimized file runtime~main.[chunkhash].js。多入口场景单页多入口用multiple各页面只加载自己那份 runtime微前端/多应用共享同一 runtime 用single。注意single下任何入口的变化都可能使共享 runtime 的 hash 变化取舍在于runtime 稳定性与入口隔离性。与 splitChunks 结合仓库中 http2-aggressive-splitting 示例 演示了另一半拼图——optimization.splitChunks配合filename: [chunkhash].js利用chunk 内容确定性让未变化 chunk 保持相同 hash它与本文的 runtime 拆分互补共同构成完整的长期缓存方案。验证手段构建后检查 stats 输出中是否存在独立的runtime~entrychunkruntime modules N modules模块表为空以及入口 chunk 中是否只剩self[webpackChunk]...push形态的代码——这是 runtime 拆分生效的直接证据可对照 chunkhash 示例的 Info 段落 中 Unoptimized 与 Production 两组输出。六、小结[chunkhash]与 Code Splitting 的矛盾根源在于 runtime含 chunk 文件名映射寄生在入口 chunk 中。optimization.runtimeChunk以极低成本一个插件、每入口一个几乎只含机制代码的小 chunk将 runtime 隔离出去再配合 HTML 内联即可让未变化即缓存的长缓存策略在整个产物集上真正成立。源码层面该选项经 normalization.js 规范化为命名函数后由 RuntimeChunkPlugin 在addEntry钩子中把runtime归属写入入口数据默认命名runtime~入口名——理解了这条链路你就能在多入口、dependOn、手动指定runtime等边界场景下做出正确配置。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。