后端缓存主要集中于“处理”步骤,通过保留数据库连接,存储处理结果等方式缩短处理时间,尽快进入“响应”步骤。当然这不在本文的讨论范围之内。
而前端缓存则可以在剩下的两步:
1、“请求”和“响应”中进行。在“请求”中,浏览器也可以通过存储结果的方式直接使用资源,直接省去了发送请求;
2、而“响应”步骤需要浏览器和服务器共同配合,通过减少响应内容来缩短传输时间。
大部分讨论缓存的文章会直接从 HTTP 协议头中的缓存字段开始,例如 Cache-Control, ETag, max-age 等。
但偶尔也会听到别人讨论 memory cache, disk cache 等。
那这两种分类体系究竟有何关联?是否有交叉?
按缓存位置分类
按缓存位置分类 (memory cache, disk cache, Service Worker 等
一个请求找资源的顺序,优先级是:(由上到下寻找,找到即返回;找不到则继续)
- Service Worker
- Memory Cache
- Disk Cache
- 网络请求
由于service worker是新出的,不算入常用范畴,最后讨论
Memory Cache: 内存缓存
几乎所有的请求资源 都能进入 memory cache;
关闭tab、关闭浏览器,该缓存就会消失;用于刷新当前网页;
1、preloader
在浏览器打开网页的过程中,会先请求 HTML 然后解析。
之后如果浏览器发现了 js, css 等需要解析和执行的资源时,它会使用 CPU 资源对它们进行解析和执行
在解析执行的时候,网络请求是空闲的,这就有了发挥的空间:我们能不能一边解析执行 js/css,一边去请求下一个(或下一批)资源呢?
这就是 preloader 要做的事情
2、preload
显式指定预加载资源
memory cache 机制保证了一个页面中如果有两个相同的请求 (例如两个 src 相同的 ,两个 href 相同的 ) 都实际只会被请求最多一次,避免浪费。
在从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置。例如页面上存在几个相同 src 的图片,即便它们可能被设置为不缓存,但依然会从 memory cache 中读取。
max-age=0 在语义上普遍被解读为“不要在下次浏览时使用”,所以和 memory cache 并不冲突
假如不想让一个资源进入缓存,就连短期也不行,那就使用 no-store。
Disk Cache:硬盘缓存
实际存在于文件系统中的,而且它允许相同的资源在跨会话,甚至跨站点的情况下使用,例如两个站点都使用了同一张图片。
disk cache 会严格根据 HTTP 头信息中的各类字段来判定:
哪些资源可以缓存,哪些资源不可以缓存;哪些资源是仍然可用的,哪些资源是过时需要重新请求的。
Service Worker
内存、硬盘缓存都是由浏览器控制,而service worker给业务开发提供了缓存的控制入口
这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在
有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空
网络请求
如果一个请求在上述 3 个位置都没有找到缓存,那么浏览器会正式发送网络请求去获取内容。
之后容易想到,为了提升之后请求的缓存命中率,自然要把这个资源添加到缓存中去。具体来说:
- 根据 Service Worker 中的 handler 决定是否存入 Cache Storage (额外的缓存位置)。
- 根据 HTTP 头部的相关字段(Cache-control, Pragma 等)决定是否存入 disk cache
- memory cache 保存一份资源 的引用,以备下次使用。
按失效策略分
我们平时最为熟悉的其实是 disk cache,也叫 HTTP cache。
平时所说的强制缓存,协商缓存,以及 Cache-Control 等,也都归于此类。
(内存缓存,浏览器控制;service worker开发者额外写的脚本;
(硬盘缓存,基于http协议约束,平易近人)

强缓存
当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在则请求真的服务器,响应后再写入缓存数据库。
强制缓存直接减少请求数,是提升最大的缓存策略。
它的优化覆盖了请求数据的全部三个步骤:请求,处理,响应
如果考虑使用缓存来优化网页性能的话,强制缓存应该是首先被考虑的。
可以造成强制缓存的字段是 Cache-control 和 Expires。
Expires
这是 HTTP 1.0 的字段,表示缓存到期时间,是一个绝对的时间
这个字段设置时有两个缺点:
- 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。
- 写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致非法属性从而设置失效。
Cache-control
已知Expires的缺点之后,在HTTP/1.1中,增加了一个字段Cache-control,该字段表示资源缓存的最大有效时间
(相对时间)
Cache-control: max-age=2592000
cache-control 字段常用的值:(完整的列表可以查看 MDN)
- max-age:即最大有效时间,在上面的例子中我们可以看到
- must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
- no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。
- no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
- public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
- private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。
协商缓存
协商缓存就是强缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识来决定是否使用缓存的过程。
主要有以下两种情况:
1、协商缓存生效,返回304
2、协商缓存失效,返回200和请求结果
有两种字段体系判断资源过期与否:
1、Last-Modified & If-Modified-Since
2、Etag & If-None-Match(1方案有些缺陷 引入的2方案)
Etag / If-None-Match优先级高于Last-Modified / If-Modified-Since,
同时存在则只有Etag / If-None-Match生效。
为什么服务器优先用etag做协商缓存判断?
- 一些文件也许会周期性的更改,但是他的内容并不改变
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,而If-Modified-Since能检查到的粒度是秒级的
Last-Modified & If-Modified-Since
文件上次修改时间
Etag & If-None-Match
etag:文件内容的hash相当于
生成 ETag 的算法并不是固定的, 通常是使用内容的散列、最后修改时间戳的哈希值或简单地使用版本号
实例分析
目前的项目大多使用这种缓存方案的:
- HTML: 协商缓存;
- css、js、图片:强缓存,文件名带上hash。
怎么设置
1、前端打包的hash
webpack给我们提供了三种哈希值计算方式,分别是hash、chunkhash和contenthash。
- hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。
- chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
- contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。
chunkhash和contenthash的主要应用场景是什么呢
一般会把项目中的css都抽离出对应的css文件来加以引用。如果我们使用chunkhash,当我们改了css代码之后,会发现css文件hash值改变的同时,js文件的hash值也会改变;
所以对于css的hash,使用contenhash
2、后端
浏览器是根据响应头的相关字段来决定缓存的方案的。
所以,后端的关键就在于,根据不同的请求返回对应的缓存字段。
以nodejs为例,,我们可以这样设置:
# 如果需要浏览器强缓存res.setHeader('Cache-Control', 'public, max-age=xxx');# 需要协商缓存res.setHeader('Cache-Control', 'public, max-age=0');res.setHeader('Last-Modified', xxx);res.setHeader('ETag', xxx);
