前端构建产物的长期缓存策略文件名哈希、模块联邦与 CDN 协同的工程实践一、缓存失效的工程代价前端资源缓存是性能优化的基础手段之一。理想状态下用户在首次访问后后续加载应命中浏览器缓存或 CDN 边缘缓存资源加载耗时趋近于 0ms。但实际工程中缓存策略通常面临两个核心矛盾更新与缓存的冲突资源更新后用户端缓存的旧版本在Cache-Control过期前不会自动更新。DNS 级别的缓存如 Service Worker可能进一步延长失效周期。缓存粒度的失衡全量无效缓存如v2.0.0查询参数导致无变更的资源也被重新下载过于精细的拆分会增加 HTTP 请求数。解决这两个矛盾的核心机制是「基于内容的文件名哈希」配合 CDN 的分发策略和构建工具的缓存优化形成三层协同的缓存体系。二、文件名哈希的策略选择文件名哈希是长期缓存的基础。构建工具Webpack、Vite/Rollup、esbuild在输出文件名中嵌入内容哈希确保内容变化时文件名自动更新。三种哈希模式的区别hash整个构建共享一个哈希值任何文件变化都导致所有文件名更新。适用于极小型项目不推荐用于生产环境。chunkhash按 chunk 粒度Webpack 概念计算哈希。同一 chunk 的 JS 和 CSS 共享哈希值。Webpack 4 的默认模式。contenthash按文件内容独立计算哈希。JS 文件的变更不影响 CSS 文件名的哈希值。目前推荐的标准模式。// ---------- Webpack 配置contenthash 策略 ---------- /** 生产环境构建配置 */ module.exports { mode: production, output: { // JS 文件使用 8 位 contenthash filename: js/[name].[contenthash:8].js, chunkFilename: js/[name].[contenthash:8].chunk.js, // 公共路径CDN 域名 publicPath: process.env.CDN_URL || /, // 清理上次构建的过期文件 clean: true, }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, css-loader, ], }, { test: /\.(png|jpe?g|gif|svg|webp)$/i, type: asset, parser: { dataUrlCondition: { // 小于 8KB 的图片内联为 base64 maxSize: 8 * 1024, }, }, generator: { filename: images/[name].[contenthash:8][ext], }, }, { test: /\.(woff2?|eot|ttf|otf)$/i, type: asset/resource, generator: { filename: fonts/[name].[contenthash:8][ext], }, }, ], }, plugins: [ // CSS 提取也使用 contenthash new MiniCssExtractPlugin({ filename: css/[name].[contenthash:8].css, chunkFilename: css/[name].[contenthash:8].chunk.css, }), ], optimization: { // 运行时代码单独提取避免频繁变更影响 vendor chunk runtimeChunk: single, moduleIds: deterministic, // 稳定的模块 ID提升长期缓存命中率 splitChunks: { chunks: all, cacheGroups: { framework: { test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/, name: framework, priority: 20, }, vendors: { test: /[\\/]node_modules[\\/]/, name: vendors, priority: 10, }, common: { minChunks: 3, priority: 5, reuseExistingChunk: true, }, }, }, }, };Vite 对应配置// vite.config.js — 生产环境哈希策略 import { defineConfig } from vite; import react from vitejs/plugin-react; export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { // Vite 默认使用内容哈希此处为显式声明 entryFileNames: js/[name].[hash:8].js, chunkFileNames: js/[name].[hash:8].js, assetFileNames: (assetInfo) { // 按资源类型分目录 if (assetInfo.name?.endsWith(.css)) { return css/[name].[hash:8][extname]; } if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(assetInfo.name || )) { return images/[name].[hash:8][extname]; } if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name || )) { return fonts/[name].[hash:8][extname]; } return assets/[name].[hash:8][extname]; }, }, // 分包策略 manualChunks: (id) { if (id.includes(node_modules/react) || id.includes(node_modules/react-dom)) { return framework; } if (id.includes(node_modules)) { return vendors; } }, }, // 启用 CSS 代码分割 cssCodeSplit: true, }, });哈希长度选择8 位十六进制哈希提供的碰撞空间为 16^8 ≈ 43 亿对于大多数项目冲突概率极低。对于 CI 每小时构建数十次的大型项目建议提升到 12 位以进一步降低碰撞概率。三、模块联邦的缓存独立性问题Webpack 5 的 Module Federation模块联邦允许运行时动态加载远程模块。这带来了新的缓存挑战远程模块的 chunk 由不同的构建系统生成其哈希值与宿主应用独立。缓存一致性需求远程模块更新后宿主应用必须感知到版本变化并加载新 chunk。宿主应用和远程模块之间共享的依赖如 React需要版本对齐避免运行时错误。回滚时宿主和远程模块必须同时回滚到兼容版本。// ---------- 模块联邦的版本管理 ---------- // 远程模块配置remote/webpack.config.js const { ModuleFederationPlugin } require(webpack/module-federation); module.exports { plugins: [ new ModuleFederationPlugin({ name: remoteApp, filename: remoteEntry.[contenthash:8].js, // 带哈希的入口 exposes: { ./Header: ./src/components/Header, ./Footer: ./src/components/Footer, }, shared: { react: { singleton: true, // 确保全局只有一个 React 实例 requiredVersion: ^18.2.0, eager: false, }, react-dom: { singleton: true, requiredVersion: ^18.2.0, eager: false, }, }, }), ], }; // 宿主应用配置host/webpack.config.js new ModuleFederationPlugin({ name: host, // 远程模块动态加载配置从版本清单文件获取最新 URL remotes: { remoteApp: remoteApp[window.__REMOTE_APP_URL__]/remoteEntry.[contenthash:8].js, }, shared: { react: { singleton: true, requiredVersion: ^18.2.0 }, react-dom: { singleton: true, requiredVersion: ^18.2.0 }, }, });版本清单文件的作用在部署远程模块时同时生成一个 JSON 清单文件记录当前的入口文件名和依赖版本。宿主应用加载前读取清单确保请求正确的哈希文件名。{ name: remoteApp, version: 2.3.1, entryFile: remoteEntry.a3f8b2c1.js, dependencies: { react: 18.2.0, react-dom: 18.2.0 }, deployedAt: 2026-07-16T08:30:00Z, checksum: sha256-xxx }四、CDN 缓存的协同策略文件名哈希解决了「更新后如何让浏览器加载新版本」的问题CDN 层面还需要处理「旧版本文件何时从边缘节点淘汰」。CDN 缓存头配置HTML 文件Cache-Control: no-cache或max-age0, must-revalidate。确保每次访问都验证 HTML 是否更新因为 HTML 中包含带哈希的资源引用。JS/CSS 文件带哈希Cache-Control: public, max-age31536000, immutable1 年。文件名变化意味着内容变化无变化则缓存永久有效。字体文件Cache-Control: public, max-age31536000, immutable。字体资源极少变更。图片文件Cache-Control: public, max-age6048007 天。图片更新频率高于 JS/CSS但远低于 HTML。# Nginx CDN 缓存头配置示例 location / { # HTML 入口文件禁止缓存 if ($uri ~* \.html$) { add_header Cache-Control no-cache, must-revalidate; expires -1; break; } # 带哈希的 JS/CSS长期缓存 if ($uri ~* \.[a-f0-9]{8,}\.(js|css)$) { add_header Cache-Control public, max-age31536000, immutable; expires 1y; break; } # 字体文件长期缓存 if ($uri ~* \.(woff2?|ttf|otf|eot)$) { add_header Cache-Control public, max-age31536000, immutable; expires 1y; break; } # 图片资源7 天缓存 if ($uri ~* \.(png|jpe?g|gif|svg|webp|ico)$) { add_header Cache-Control public, max-age604800; expires 7d; break; } }CDN 的版本管理每次构建完成后所有带哈希的资源文件名都是全新的旧文件持续占据 CDN 存储。需要定期清理策略基于时间的清理保留最近 N 次部署的构建产物如 10 次清理更早的文件。基于引用计数的清理从 HTML 入口文件反向解析引用的资源路径清理不在引用集合中的文件。// ---------- 部署后 CDN 清理脚本 ---------- /** * CDN 过期资源清理 * 原理读取当前 HTML 中引用的所有资源路径删除不在引用集中的旧文件 */ async function cleanCDNArtifacts( htmlFilePath: string, // 当前部署的 HTML 文件路径 cdnBucket: string // CDN 存储桶路径 ): PromiseCleanResult { const fs require(fs); const path require(path); const html fs.readFileSync(htmlFilePath, utf-8); // 从 HTML 中提取所有资源引用 const assetPatterns [ /src[]([^])[]/g, /href[]([^])[]/g, /url\([]?([^)])[]?\)/g, ]; const currentAssets new Setstring(); for (const pattern of assetPatterns) { let match; while ((match pattern.exec(html)) ! null) { // 提取文件名 const url match[1]; // 只处理带哈希的文件排除外部 URL 和 data URL if (url.includes(.) !url.startsWith(http) !url.startsWith(data:)) { currentAssets.add(path.basename(url)); } } } // 统计结果实际清理操作需要对接 CDN API return { currentAssetCount: currentAssets.size, message: 当前版本使用 ${currentAssets.size} 个资源文件, }; }五、总结构建产物的长期缓存依赖三个环节的协同构建层使用contenthash确保只有内容变化的文件才更新文件名。配套策略包括runtimeChunk单独提取、moduleIds: deterministic确保模块 ID 稳定。部署层HTML 使用短期缓存no-cache带哈希的静态资源使用长期缓存max-age31536000, immutable。清理层CDN 上的历史构建产物需要定期清理避免存储膨胀。清理逻辑应基于「当前版本引用集合」做差分删除。模块联邦场景的额外要求远程模块需要生成版本清单文件宿主应用在运行时动态读取确保加载与清单匹配的哈希文件名。这一层如果缺失远程模块更新后宿主可能因文件名不匹配而加载失败。
前端构建产物的长期缓存策略:文件名哈希、模块联邦与 CDN 协同的工程实践
前端构建产物的长期缓存策略文件名哈希、模块联邦与 CDN 协同的工程实践一、缓存失效的工程代价前端资源缓存是性能优化的基础手段之一。理想状态下用户在首次访问后后续加载应命中浏览器缓存或 CDN 边缘缓存资源加载耗时趋近于 0ms。但实际工程中缓存策略通常面临两个核心矛盾更新与缓存的冲突资源更新后用户端缓存的旧版本在Cache-Control过期前不会自动更新。DNS 级别的缓存如 Service Worker可能进一步延长失效周期。缓存粒度的失衡全量无效缓存如v2.0.0查询参数导致无变更的资源也被重新下载过于精细的拆分会增加 HTTP 请求数。解决这两个矛盾的核心机制是「基于内容的文件名哈希」配合 CDN 的分发策略和构建工具的缓存优化形成三层协同的缓存体系。二、文件名哈希的策略选择文件名哈希是长期缓存的基础。构建工具Webpack、Vite/Rollup、esbuild在输出文件名中嵌入内容哈希确保内容变化时文件名自动更新。三种哈希模式的区别hash整个构建共享一个哈希值任何文件变化都导致所有文件名更新。适用于极小型项目不推荐用于生产环境。chunkhash按 chunk 粒度Webpack 概念计算哈希。同一 chunk 的 JS 和 CSS 共享哈希值。Webpack 4 的默认模式。contenthash按文件内容独立计算哈希。JS 文件的变更不影响 CSS 文件名的哈希值。目前推荐的标准模式。// ---------- Webpack 配置contenthash 策略 ---------- /** 生产环境构建配置 */ module.exports { mode: production, output: { // JS 文件使用 8 位 contenthash filename: js/[name].[contenthash:8].js, chunkFilename: js/[name].[contenthash:8].chunk.js, // 公共路径CDN 域名 publicPath: process.env.CDN_URL || /, // 清理上次构建的过期文件 clean: true, }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, css-loader, ], }, { test: /\.(png|jpe?g|gif|svg|webp)$/i, type: asset, parser: { dataUrlCondition: { // 小于 8KB 的图片内联为 base64 maxSize: 8 * 1024, }, }, generator: { filename: images/[name].[contenthash:8][ext], }, }, { test: /\.(woff2?|eot|ttf|otf)$/i, type: asset/resource, generator: { filename: fonts/[name].[contenthash:8][ext], }, }, ], }, plugins: [ // CSS 提取也使用 contenthash new MiniCssExtractPlugin({ filename: css/[name].[contenthash:8].css, chunkFilename: css/[name].[contenthash:8].chunk.css, }), ], optimization: { // 运行时代码单独提取避免频繁变更影响 vendor chunk runtimeChunk: single, moduleIds: deterministic, // 稳定的模块 ID提升长期缓存命中率 splitChunks: { chunks: all, cacheGroups: { framework: { test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/, name: framework, priority: 20, }, vendors: { test: /[\\/]node_modules[\\/]/, name: vendors, priority: 10, }, common: { minChunks: 3, priority: 5, reuseExistingChunk: true, }, }, }, }, };Vite 对应配置// vite.config.js — 生产环境哈希策略 import { defineConfig } from vite; import react from vitejs/plugin-react; export default defineConfig({ plugins: [react()], build: { rollupOptions: { output: { // Vite 默认使用内容哈希此处为显式声明 entryFileNames: js/[name].[hash:8].js, chunkFileNames: js/[name].[hash:8].js, assetFileNames: (assetInfo) { // 按资源类型分目录 if (assetInfo.name?.endsWith(.css)) { return css/[name].[hash:8][extname]; } if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(assetInfo.name || )) { return images/[name].[hash:8][extname]; } if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name || )) { return fonts/[name].[hash:8][extname]; } return assets/[name].[hash:8][extname]; }, }, // 分包策略 manualChunks: (id) { if (id.includes(node_modules/react) || id.includes(node_modules/react-dom)) { return framework; } if (id.includes(node_modules)) { return vendors; } }, }, // 启用 CSS 代码分割 cssCodeSplit: true, }, });哈希长度选择8 位十六进制哈希提供的碰撞空间为 16^8 ≈ 43 亿对于大多数项目冲突概率极低。对于 CI 每小时构建数十次的大型项目建议提升到 12 位以进一步降低碰撞概率。三、模块联邦的缓存独立性问题Webpack 5 的 Module Federation模块联邦允许运行时动态加载远程模块。这带来了新的缓存挑战远程模块的 chunk 由不同的构建系统生成其哈希值与宿主应用独立。缓存一致性需求远程模块更新后宿主应用必须感知到版本变化并加载新 chunk。宿主应用和远程模块之间共享的依赖如 React需要版本对齐避免运行时错误。回滚时宿主和远程模块必须同时回滚到兼容版本。// ---------- 模块联邦的版本管理 ---------- // 远程模块配置remote/webpack.config.js const { ModuleFederationPlugin } require(webpack/module-federation); module.exports { plugins: [ new ModuleFederationPlugin({ name: remoteApp, filename: remoteEntry.[contenthash:8].js, // 带哈希的入口 exposes: { ./Header: ./src/components/Header, ./Footer: ./src/components/Footer, }, shared: { react: { singleton: true, // 确保全局只有一个 React 实例 requiredVersion: ^18.2.0, eager: false, }, react-dom: { singleton: true, requiredVersion: ^18.2.0, eager: false, }, }, }), ], }; // 宿主应用配置host/webpack.config.js new ModuleFederationPlugin({ name: host, // 远程模块动态加载配置从版本清单文件获取最新 URL remotes: { remoteApp: remoteApp[window.__REMOTE_APP_URL__]/remoteEntry.[contenthash:8].js, }, shared: { react: { singleton: true, requiredVersion: ^18.2.0 }, react-dom: { singleton: true, requiredVersion: ^18.2.0 }, }, });版本清单文件的作用在部署远程模块时同时生成一个 JSON 清单文件记录当前的入口文件名和依赖版本。宿主应用加载前读取清单确保请求正确的哈希文件名。{ name: remoteApp, version: 2.3.1, entryFile: remoteEntry.a3f8b2c1.js, dependencies: { react: 18.2.0, react-dom: 18.2.0 }, deployedAt: 2026-07-16T08:30:00Z, checksum: sha256-xxx }四、CDN 缓存的协同策略文件名哈希解决了「更新后如何让浏览器加载新版本」的问题CDN 层面还需要处理「旧版本文件何时从边缘节点淘汰」。CDN 缓存头配置HTML 文件Cache-Control: no-cache或max-age0, must-revalidate。确保每次访问都验证 HTML 是否更新因为 HTML 中包含带哈希的资源引用。JS/CSS 文件带哈希Cache-Control: public, max-age31536000, immutable1 年。文件名变化意味着内容变化无变化则缓存永久有效。字体文件Cache-Control: public, max-age31536000, immutable。字体资源极少变更。图片文件Cache-Control: public, max-age6048007 天。图片更新频率高于 JS/CSS但远低于 HTML。# Nginx CDN 缓存头配置示例 location / { # HTML 入口文件禁止缓存 if ($uri ~* \.html$) { add_header Cache-Control no-cache, must-revalidate; expires -1; break; } # 带哈希的 JS/CSS长期缓存 if ($uri ~* \.[a-f0-9]{8,}\.(js|css)$) { add_header Cache-Control public, max-age31536000, immutable; expires 1y; break; } # 字体文件长期缓存 if ($uri ~* \.(woff2?|ttf|otf|eot)$) { add_header Cache-Control public, max-age31536000, immutable; expires 1y; break; } # 图片资源7 天缓存 if ($uri ~* \.(png|jpe?g|gif|svg|webp|ico)$) { add_header Cache-Control public, max-age604800; expires 7d; break; } }CDN 的版本管理每次构建完成后所有带哈希的资源文件名都是全新的旧文件持续占据 CDN 存储。需要定期清理策略基于时间的清理保留最近 N 次部署的构建产物如 10 次清理更早的文件。基于引用计数的清理从 HTML 入口文件反向解析引用的资源路径清理不在引用集合中的文件。// ---------- 部署后 CDN 清理脚本 ---------- /** * CDN 过期资源清理 * 原理读取当前 HTML 中引用的所有资源路径删除不在引用集中的旧文件 */ async function cleanCDNArtifacts( htmlFilePath: string, // 当前部署的 HTML 文件路径 cdnBucket: string // CDN 存储桶路径 ): PromiseCleanResult { const fs require(fs); const path require(path); const html fs.readFileSync(htmlFilePath, utf-8); // 从 HTML 中提取所有资源引用 const assetPatterns [ /src[]([^])[]/g, /href[]([^])[]/g, /url\([]?([^)])[]?\)/g, ]; const currentAssets new Setstring(); for (const pattern of assetPatterns) { let match; while ((match pattern.exec(html)) ! null) { // 提取文件名 const url match[1]; // 只处理带哈希的文件排除外部 URL 和 data URL if (url.includes(.) !url.startsWith(http) !url.startsWith(data:)) { currentAssets.add(path.basename(url)); } } } // 统计结果实际清理操作需要对接 CDN API return { currentAssetCount: currentAssets.size, message: 当前版本使用 ${currentAssets.size} 个资源文件, }; }五、总结构建产物的长期缓存依赖三个环节的协同构建层使用contenthash确保只有内容变化的文件才更新文件名。配套策略包括runtimeChunk单独提取、moduleIds: deterministic确保模块 ID 稳定。部署层HTML 使用短期缓存no-cache带哈希的静态资源使用长期缓存max-age31536000, immutable。清理层CDN 上的历史构建产物需要定期清理避免存储膨胀。清理逻辑应基于「当前版本引用集合」做差分删除。模块联邦场景的额外要求远程模块需要生成版本清单文件宿主应用在运行时动态读取确保加载与清单匹配的哈希文件名。这一层如果缺失远程模块更新后宿主可能因文件名不匹配而加载失败。