浏览器的存储和缓存机制就是为了减少网格请求,提高用户体验。
其中缓存位置上来说分为四种:Memory Cache,Service Worker Cache,HTTP Cache, Push Cache.并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
Memory Cache 也就是内存中的缓存,其优点是读取速度快,但是一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
浏览器秉承的是“节约原则”。我们发现,Base64 格式的图片,几乎永远可以被塞进 memory cache。此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件由于内存资源是有限的,它们往往被直接甩进磁盘。
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。 但是传输协议必须为 https。
Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。我们不常用到。
Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。详情请查看HTTP/2 push is tougher than I thought。
HTTP 缓存是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control
// 处理图片,强制性缓存
router.get(/\S*\.(jpe?g|png)$/, async (ctx, next) => {
const { path } = ctx;
ctx.type = mime.getType(path);
// Expires:缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。
ctx.set('expires', new Date(Date.now() + 30000).toUTCString())
/* Cache-Control
public:表示响应可以被客户端和代理服务器缓存
private:表示响应可以被客户端缓存、
max-age=1000:缓存30秒后就过期,需要重新请求。
s-max-age=1000:覆盖max-age,作用一样,只是在代理服务器生效
no-store:不缓存任何响应
no-cache: 资源被缓存,但是立即失效,下次会发起请求验证资源是否过期。
max-stale=1000:1000秒内,即使缓存过期,也使用该缓存
max-fresh=1000:希望在1000秒内获取最新的响应
*/
ctx.set('cache-control', 'no-cache,public,max-age=1000');
const imageBuffer = await fs.readFile(Path.resolve(__dirname, `.${path}`));
ctx.body = imageBuffer;
await next();
});
协商缓存依赖于服务端与浏览器之间的通信。
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源
如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)。
协商缓存中那几个首部字段是配对使用的,即:
if-modified-since 和 last-modified
它俩的值都是 GMT 格式的精确到秒的时间值。从字面上就很好理解它们的含义:自从某某时间有没有修改过?
,最后一次修改时间为某某时间
。
他俩有啥关系呢?其实本次请求头 if-modified-since的值应该为上一次请求该资源的响应头中 last-modified 的值。
当浏览器发起资源请求并携带 if-modified-since 字段,服务器会将请求头中的 if-modified-since 值和请求资源的 最后修改时间进行比较,如果资源最后修改时间比 if-modified-since 时间晚,那么资源过期,状态码为 200,响应体为请求资源,响应头中加入最新的 last-modified 的值。没过期就返回状态码 304,命中协商缓存,响应体为空,响应头不需要 last-modified 值。
// 处理 css 文件 last-modified 配置协商缓存
router.get(/\S*\.css$/, async (ctx, next) => {
const { request, response, path } = ctx;
response.set('pragma', 'no-cache');
const cssPath = Path.resolve(__dirname, `.${path}`);
const ifModifiedSince = request.headers['if-modified-since'];
const cssStatus = await fs.stat(cssPath);
const lastModified = cssStatus.mtime.toGMTString();
if (ifModifiedSince === lastModified) {
response.status = 304;
} else {
response.lastModified = lastModified;
await responseFile(cssPath, ctx);
}
await next();
});
if-none-match 和响应头 etag
Etag就像个key值,
资源变化都会导致ETag变化,ETag
可以保证每一个资源是唯一的key。
MDN 上对 etag 的描述是:
它们是位于双引号之间的ASCII字符串(如“675af34563dc-tr34”)。 没有明确指定生成ETag值的方法。 通常,使用内容的散列,最后修改时间戳的哈希值,或简单地使用版本号。 例如,MDN使用wiki内容的十六进制数字的哈希值。
为什么有了 last-modified 还需要 etag ?
- 资源在 1 秒内更新,并且在该一秒内访问,使用 last-modified 处理协商缓存无法获取最新资源。本质上的原因还是因为 last-modified 是精确到秒的,无法反映在 1 秒内的变化。
- 当资源多次被修改后内容不变,使用 last-modified 来处理有点浪费。多次修改资源,其 last-modified 值肯定是会变的,但是如果内容不变我们其实不需要服务器返回最新资源,直接使用本地缓存。使用 etag 就没这个问题,因为同一个资源多次修改,内容一样, hash 值也一样。
- 使用 etag 更加灵活,因为 etag 并不一定是我说的就用 hash 值,etag 采用的是弱比较算法,即两个文件除了每个比特都相同外,内容一致也可以认为是相同的。例如,如果两个页面仅仅在页脚的生成时间有所不同,就可以认为二者是相同的。
使用 etag 配置协商缓存
/ 处理 js 文件 使用 etag 配置协商缓存
router.get(/\S*\.js$/, async (ctx, next) => {
const { request, response, path } = ctx;
ctx.type = mime.getType(path);
response.set('pragma', 'no-cache');
const ifNoneMatch = request.headers['if-none-match'];
const imagePath = Path.resolve(__dirname, `.${path}`);
const hash = crypto.createHash('md5');
const imageBuffer = await fs.readFile(imagePath);
hash.update(imageBuffer);
const etag = `"${hash.digest('hex')}"`;
if (ifNoneMatch === etag) {
response.status = 304;
} else {
response.set('etag', etag);
ctx.body = imageBuffer;
}
await next();
});
5、如何选择适合的缓存
整体流程图(来源网上)
参考资源:
1、浏览器缓存:memory cache、disk cache、强缓存协商缓存等概念
2、前端性能优化原理与实践
3、理解http浏览器的协商缓存和强制缓存
4、缓存(二)——浏览器缓存机制:强缓存、协商缓存
github