一、什么是文件指纹?
核心思路(一句话)
文件指纹就是将资源内容映射为 Hash,并将 Hash 写入资源 URL,使内容变化产生新 URL,从而实现静态资源长期缓存。
text
资源内容 → Hash → 文件名 → 长期缓存
│
┌──────────┴──────────┐
内容不变 内容变化
↓ ↓
URL不变 URL变化
↓ ↓
命中缓存 下载新资源
底层原理
浏览器缓存最终是围绕 URL 对应的资源。
因此:
text
/app.js
即使服务器内容已经更新,只要浏览器仍然使用旧缓存,就存在版本不一致问题。
而:
text
/app.a81f3c.js
/app.72bc91.js
是两个不同 URL。
因此可以采用:
http
Cache-Control: public, max-age=31536000, immutable
让带指纹的静态资源长期缓存。
真正解决问题的是"URL 随内容变化",Hash 只是实现这个机制的手段。
二、Webpack 三种 Hash 的准确区别
注意:这里说的
hash、chunkhash、contenthash是 Webpack 输出文件名模板中的三种 Hash 占位符。不同 Webpack 版本的内部实现细节有所演进,因此面试时应抓住"依赖范围"和"缓存粒度",不要简单说成三个 Hash 算法。
| 类型 | 技术含义 | 影响范围 | 缓存粒度 |
|---|---|---|---|
hash |
基于整个 Compilation 的构建结果计算的 Hash | 整个构建 | 最粗 |
chunkhash |
基于 Chunk 的内容及其相关依赖计算的 Hash | Chunk | 中等 |
contenthash |
基于最终输出资源内容计算的 Hash | 单个资源 | 最细 |
1. hash
text
整个 Compilation
↓
hash
↓
多个输出资源可能使用同一个 Hash
例如:
text
main.a81f3c.js
vendor.a81f3c.js
只要整个构建发生变化,使用该 hash 的资源文件名就可能一起变化。
问题:缓存粒度太粗。
2. chunkhash
text
Chunk A → chunkhash A
Chunk B → chunkhash B
例如:
text
main.a81f3c.js
vendor.72bc91.js
修改 main 所属 Chunk:
text
main.a81f3c.js → main.91def2.js
vendor.72bc91.js → 通常保持不变
它比 hash 更适合缓存,因为变化被限制在 Chunk 范围。
但它仍然是 Chunk 粒度,不是最终输出文件粒度。
另外,
chunkhash在现代 Webpack 中已被标记为过时方向,官方推荐使用contenthash。
3. contenthash
text
最终输出资源
↓
资源内容
↓
contenthash
例如:
text
main.a81f3c.js
main.72bc91.css
如果只修改 CSS:
text
CSS内容变化
→ CSS contenthash变化
JS最终内容没变
→ JS contenthash保持不变
因此 JavaScript 和 CSS 可以拥有独立的缓存生命周期。
这正是 contenthash 适合长期缓存的核心原因。
三、三种 Hash 的本质区别
不要死记"全局、Chunk、文件"三个词,面试直接抓住:
text
hash
→ 依赖整个构建
chunkhash
→ 依赖 Chunk
contenthash
→ 依赖最终资源内容
也就是:
Hash 的关键区别不是 Hash 算法不同,而是"
参与计算的依赖范围不同"。
最终目的都是:
text
依赖范围越精确
↓
无关资源越不容易被连带修改文件名
↓
缓存复用率越高
四、为什么生产环境还需要关注 Runtime?
这是文件指纹题非常容易被忽略的高级问题。
Webpack 会生成一部分 Runtime 代码,负责模块加载、模块映射、动态 Chunk 加载等运行时信息。
例如:
text
main.js
vendor.js
runtime.js
Runtime 中可能保存:
text
模块ID
Chunk映射
异步Chunk文件名
加载逻辑
因此当 Chunk 文件名发生变化时:
text
业务代码变化
↓
Chunk Hash变化
↓
Chunk文件名变化
↓
Runtime中的Chunk映射也可能变化
↓
Runtime内容变化
↓
runtime.[contenthash].js变化
于是可能出现:
text
vendor.abc.js ← 内容没变
runtime.123.js ← 变化
main.456.js ← 变化
这样 Runtime 自己的缓存也会频繁失效。
五、如何降低 Runtime 对长期缓存的影响?
核心方案
javascript
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
},
}
把 Runtime 独立出来:
text
应用
│
┌─────────┼─────────┐
↓ ↓ ↓
runtime vendor business
│ │ │
↓ ↓ ↓
runtime.x vendor.x main.x
这样不同代码的变化被隔离。
例如:
text
修改业务代码
↓
business Hash变化
vendor
↓
保持缓存
runtime
↓
只有运行时映射确实发生变化时才变化
但要注意
runtimeChunk: "single" 不是保证 Runtime 永远不变。
如果 Chunk 文件名、模块关系、Chunk 映射发生变化,Runtime 仍然可能变化。
因此正确理解是:
独立 Runtime 是为了缩小变化传播范围,而不是让 Runtime 永久稳定。
六、生产环境完整缓存策略
text
HTML
↓
短缓存 / 协商缓存
↓
引用带 Content Hash 的静态资源
↓
contenthash
+
代码分割
+
splitChunks
+
runtimeChunk
↓
JS / CSS / 图片 / 字体
↓
长期强缓存
其中:
text
HTML
→ 获取最新资源清单
带 Hash 的静态资源
→ 内容不变就一直缓存
→ 内容变化就通过新 URL 获取
七、容易被问到的两个边界
Hash 是否绝对唯一?
不是。Hash 存在碰撞可能,只是实际工程中通过足够合理的 Hash 长度和算法,将碰撞概率控制在可接受范围。
文件指纹是否用于安全校验?
主要不是。文件指纹解决:
text
缓存失效
+
资源版本管理
安全完整性校验属于其他机制,例如 Subresource Integrity。
八、最终记忆模型
text
文件指纹
│
├── 目的
│ └── 内容变化 → URL变化 → 长期缓存
│
├── Hash粒度
│ ├── hash → Compilation
│ ├── chunkhash → Chunk
│ └── contenthash → 最终资源
│
└── 生产优化
├── contenthash
├── splitChunks
└── runtimeChunk
满分答案
Webpack 文件指纹的核心,是让资源内容决定 URL,从而解决长期缓存和资源更新之间的矛盾。
三种 Hash 的区别在于参与计算的依赖范围:
text
hash → 整个 Compilation
chunkhash → Chunk
contenthash → 最终输出资源内容
hash 粒度最大,容易导致无关资源一起失效;chunkhash 缩小到 Chunk;contenthash 进一步缩小到具体资源,因此现代 Webpack 的长期缓存通常优先使用 contenthash。
同时要注意 Webpack Runtime:它保存模块和 Chunk 的运行时映射,Chunk 文件名变化可能导致 Runtime 内容变化。因此生产环境通常结合:
text
contenthash + splitChunks + runtimeChunk
将变化频率不同的资源隔离。
最终目标只有一个:资源没变就复用缓存,资源变了才生成新的 URL。