【前端面试小册】网络-第4节:缓存实践与进阶(百度,字节面试题)
一、面试题
通过强缓存与协商缓存部分我们已经知道通过缓存可以给页面访问提速,提升用户体验,还可以节省流量,减轻服务器压力。
但是问题来了:
- 在实际项目中如何保证更新版本的时候,客户端缓存失效?
- HTML、CSS、JS 以及图片等资源,那些使用强缓存,那些使用协商缓存?
- hash、chunkhash 和 contenthash 的是什么?他们的区别又是什么?
二、问题 1:解决版本更新时缓存失效
2.1 问题分析
核心问题:当代码更新后,如果使用强缓存,客户端可能仍然使用旧版本的资源,导致更新不生效。
2.2 解决办法
方法一:更新静态资源的路径
原理:通过改变资源路径(文件名),强制浏览器重新下载新资源。
实现方式:
- 更新前端资源的时候,需要更改静态资源的路径
- HTML 资源使用协商缓存,这样每次请求网站,客户端都会与服务端通信,对比 HTML 是否发生改变
- 这样一旦更新静态资源后,协商获取的 HTML 会请求新的 JS/CSS 等静态资源
示例:
<!-- 旧版本 -->
<script src="/js/app.js"></script>
<!-- 新版本 -->
<script src="/js/app.v2.js"></script>
方法二:使用文件 hash
原理:文件内容改变,hash 值改变,文件名改变,浏览器会重新下载。
示例:
<!-- 构建时自动生成 hash -->
<script src="/js/app.a1b2c3d4.js"></script>
<!-- 文件内容改变后 -->
<script src="/js/app.e5f6g7h8.js"></script>
2.3 实现流程
graph TD
A[代码更新] --> B[构建生成新文件]
B --> C[文件名包含 hash]
C --> D[HTML 使用协商缓存]
D --> E[客户端请求 HTML]
E --> F[服务器返回新 HTML]
F --> G[HTML 中包含新资源路径]
G --> H[浏览器请求新资源]
H --> I[旧资源缓存失效]
三、问题 2:资源缓存策略
3.1 缓存策略选择
HTML 走协商缓存
原因:
- HTML 是入口文件,需要及时更新
- HTML 中包含了其他资源的引用路径
- 使用协商缓存可以及时获取更新后的 HTML
设置方式:
// Node.js
ctx.set('Cache-Control', 'no-cache');
// 或
ctx.set('Cache-Control', 'max-age=0, must-revalidate');
CSS、JS、图片使用强缓存,文件命名带上 hash 值
原因:
- 这些资源更新频率相对较低
- 文件名带 hash,内容改变时文件名改变
- 使用强缓存可以提升性能
设置方式:
// Node.js
ctx.set('Cache-Control', 'max-age=31536000'); // 1 年
文件命名:
<!-- 构建时自动生成 -->
<link rel="stylesheet" href="/css/main.a1b2c3d4.css">
<script src="/js/app.e5f6g7h8.js"></script>
<img src="/images/logo.f9g0h1i2.png">
3.2 完整缓存策略
| 资源类型 | 缓存策略 | 文件名 | 原因 |
|---|---|---|---|
| HTML | 协商缓存 | 不带 hash | 需要及时更新,包含资源路径 |
| CSS | 强缓存 | 带 hash | 更新频率低,提升性能 |
| JS | 强缓存 | 带 hash | 更新频率低,提升性能 |
| 图片 | 强缓存 | 带 hash | 更新频率低,提升性能 |
| 字体 | 强缓存 | 带 hash | 更新频率低,提升性能 |
| API 数据 | 根据业务 | 不适用 | 根据数据更新频率决定 |
四、问题 3:hash、chunkhash 和 contenthash
4.1 hash
定义
跟整个项目的构建相关,构建生成的文件 hash 值都是一样的,只要项目里有文件更改,整个项目构建的 hash 值都会更改。
特点:
- 所有文件共享同一个 hash
- 任何文件改变,所有文件的 hash 都改变
- 不利于缓存优化
示例:
// webpack.config.js
module.exports = {
output: {
filename: '[name].[hash].js'
}
};
// 构建结果
// main.abc123.js
// vendor.abc123.js // 所有文件 hash 相同
问题:如果只修改了一个文件,所有文件的 hash 都会改变,导致所有缓存失效。
4.2 chunkhash
定义
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。
特点:
- 每个 chunk 有独立的 hash
- 只有该 chunk 内的文件改变,hash 才改变
- 适合分离公共库和业务代码
使用场景
一般用在生产环境对公共库和程序入口文件单独抽离开,单独打包构建,用 chunkhash 的方式对这些打包后的文件带上相应 hash 值。
在线上,只要公共库和入口没变,其 hash 值就不会改变,从而达到缓存的目的。
配置示例
entry: {
main: path.join(__dirname, './main.js'),
vendor: ['vue']
},
output: {
path: path.join(__dirname, './dist'),
publicPath: '/dist/',
filename: 'bundle.[chunkhash].js'
}
说明:
- 此时入口有两个:
main和vendor - 此时的打包就会生成
main和vendor两个 hash 值 - 这两个 hash 值是分别以
main和vendor为入口出发根据依赖文件构建生成的 chunk 和 hash 值 - chunk 可以理解为入口 + 它依赖的文件构成的模块
构建结果:
main.abc123.js // main chunk 的 hash
vendor.def456.js // vendor chunk 的 hash
4.3 contenthash
定义
由文件内容产生的 hash 值,内容不同产生的 contenthash 值也不一样。
特点:
- 每个文件有独立的 hash
- 只有文件内容改变,hash 才改变
- 最适合缓存优化
使用场景
在 webpack 中的用法一般是分离 JS 和 CSS,单独对 CSS 文件来设置。
原因:chunkhash 存在一个问题:
chunkhash 以入口和依赖文件构建的 chunk 模块,只要对应其中一个 CSS 或者 JS 改变,与其关联的文件 hash 值也会改变,但其他内容并没有改变,一定意义上的缓存也就失效。
示例:
- JS 文件逻辑改变了,但是我 CSS 样式并没有改动
- CSS 文件就应该从缓存中加载,不应该从服务器中重新获取
- 浪费带宽和性能
所以在项目中,通常做法是把项目中 CSS 都抽离出对应的 CSS 文件来加以引用。
配置示例
const miniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: {
main: path.join(__dirname, './main.js'),
vendor: ['vue']
},
output: {
path: path.join(__dirname, './dist'),
publicPath: '/dist/',
filename: 'bundle.[chunkhash].js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
miniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: 'main.[contenthash:7].css'
})
]
};
说明:
main模块用了chunkhash- 这里单独把
main的 CSS 抽离出来用contenthash处理 - 打包后即使 CSS 文件所处的模块里就算其他文件内容改变,只要 CSS 文件内容不变,那么就不会重复构建
构建结果:
bundle.abc123.js // JS 使用 chunkhash
main.def4567.css // CSS 使用 contenthash
4.4 三种 hash 对比
| hash 类型 | 作用范围 | 改变条件 | 适用场景 | 缓存效果 |
|---|---|---|---|---|
| hash | 整个项目 | 任何文件改变 | 不推荐 | 差 |
| chunkhash | 单个 chunk | chunk 内文件改变 | 分离公共库 | 中 |
| contenthash | 单个文件 | 文件内容改变 | CSS 文件 | 优 |
4.5 最佳实践
module.exports = {
output: {
// JS 使用 chunkhash
filename: 'js/[name].[chunkhash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].chunk.js'
},
plugins: [
new MiniCssExtractPlugin({
// CSS 使用 contenthash
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
})
]
};
五、完整缓存方案
5.1 资源分类和策略
// 缓存配置
const cacheConfig = {
// HTML:协商缓存
html: {
'Cache-Control': 'no-cache',
'ETag': true
},
// 静态资源:强缓存 + hash
static: {
'Cache-Control': 'max-age=31536000', // 1 年
'ETag': false
},
// API:根据业务决定
api: {
'Cache-Control': 'max-age=300', // 5 分钟
'ETag': true
}
};
5.2 构建配置
// webpack.config.js
module.exports = {
output: {
// 文件名带 hash
filename: 'js/[name].[chunkhash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].chunk.js',
assetModuleFilename: 'images/[name].[hash:8][ext]'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
}),
new HtmlWebpackPlugin({
template: './index.html',
// HTML 文件名不带 hash
filename: 'index.html'
})
]
};
5.3 服务器配置
// Node.js 示例
app.use(express.static('dist', {
maxAge: '1y', // 静态资源 1 年缓存
etag: false // 静态资源不使用 etag
}));
// HTML 特殊处理
app.get('*.html', (req, res, next) => {
res.set('Cache-Control', 'no-cache');
res.set('ETag', generateETag(req.path));
next();
});
六、实际应用场景
6.1 单页应用(SPA)
// React/Vue 项目
// HTML:协商缓存
// JS/CSS:强缓存 + hash
// 图片:强缓存 + hash
// 构建结果
index.html // 协商缓存
main.abc123.js // 强缓存
vendor.def456.js // 强缓存
app.ghi789.css // 强缓存
6.2 多页应用(MPA)
// 每个页面独立
page1.html // 协商缓存
page1.abc123.js // 强缓存
page2.html // 协商缓存
page2.def456.js // 强缓存
6.3 版本更新流程
graph TD
A[代码更新] --> B[构建项目]
B --> C[生成带 hash 的文件]
C --> D[部署到服务器]
D --> E[用户访问]
E --> F[HTML 协商缓存获取]
F --> G[HTML 包含新资源路径]
G --> H[浏览器请求新资源]
H --> I[旧资源缓存失效]
七、常见问题
7.1 为什么 HTML 不能用强缓存?
原因:
- HTML 是入口文件,包含资源路径
- 如果 HTML 使用强缓存,更新后用户可能仍使用旧 HTML
- 旧 HTML 引用旧资源,导致更新不生效
7.2 为什么静态资源要带 hash?
原因:
- 文件名改变,浏览器会重新下载
- 即使使用强缓存,也能保证更新生效
- 内容不变时,hash 不变,继续使用缓存
7.3 如何选择 hash 类型?
建议:
- JS:使用
chunkhash(分离公共库) - CSS:使用
contenthash(独立缓存) - 图片:使用
hash或contenthash - 不推荐:项目级别
hash(缓存效果差)
八、面试要点总结
核心知识点
- 版本更新:通过改变资源路径(hash)保证缓存失效
- 缓存策略:HTML 协商缓存,静态资源强缓存
- hash 类型:hash、chunkhash、contenthash 的区别
- 最佳实践:根据资源类型选择合适的缓存策略和 hash 类型
常见面试题
Q1: 如何保证版本更新时缓存失效?
答:
- HTML 使用协商缓存,确保能获取最新 HTML
- 静态资源文件名带 hash,内容改变时文件名改变
- HTML 中包含新资源路径,浏览器会请求新资源
Q2: 不同资源应该使用什么缓存策略?
答:
- HTML:协商缓存(
no-cache) - CSS/JS/图片:强缓存(
max-age),文件名带 hash
Q3: hash、chunkhash、contenthash 的区别?
答:
- hash:整个项目共享,任何文件改变都改变
- chunkhash:每个 chunk 独立,chunk 内文件改变才改变
- contenthash:每个文件独立,文件内容改变才改变
实战建议
- ✅ 理解三种 hash 的区别和使用场景
- ✅ 根据资源类型选择合适的缓存策略
- ✅ HTML 必须使用协商缓存
- ✅ 静态资源使用强缓存 + hash
查看14道真题和解析