缓存
是提高数据读取读取性能的技术,在软件开发中广泛使用,比如常见的CPU
缓存、数据库缓存、浏览器缓存等。
缓存
是Web
前端性能优化的必要手段之一,既能保证用户在第一时间里获取到最新的资源,又能减少网络请求。浏览器缓存策略主要包括以下两种:
HTTP缓存
ServiceWorker
HTTP 缓存
HTTP
缓存需要浏览器和服务器进行配置使用,比如Nginx
、Apache
可以设置不同的HTTP
缓存策略。HTTP
缓存主要是对浏览器的静态文件起作用,分为强制缓存
和协商缓存
。
强制缓存
强制缓存
策略是浏览器根据资源过期时间来决定,是否使用缓存。如果在缓存中找到资源,没有过期,则直接使用缓存资源,状态码是200
, 否则直接进行请求。
强制缓存
是根据Expires
和 Cache-control
来决定的。
Expires
HTTP/1.0
中可以使用响应头部字段 Expires
来设置缓存时间,它对应一个未来的时间戳。
expires: Thu, 06 Mar 2031 12:35:43 GMT
Expires
有一个致命的缺点是: 它采用的是时间是以服务器的为准,但是浏览器进行判断是将本地的时间与此时间进行对比,这样会导致时间精度上存在误差。因此为了为了更精准的控制资源,http/1.1
新增了Cache-control
响应头字段。
Cache-control
Cache-control
是新增了http/1.1
响应头字段,用来控制浏览器强制缓存
。
它的常用值有下面几个:
字段 | 说明 |
---|---|
no-cache | 表示使用协商缓存,即每次使用缓存前必须向服务端确认缓存资源是否更新; |
no-store | 禁止浏览器以及所有中间缓存存储响应内容 |
public | 公有缓存,表示可以被代理服务器缓存,可以被多个用户共享 |
private | 私有缓存,不能被代理服务器缓存,不可以被多个用户共享 |
max-age | 以秒为单位的数值,表示缓存的有效时间 |
must-revalidate | 当缓存过期时,需要去服务端校验缓存的有效性 |
其值可以组合使用:
cache-control: public, max-age=31536000
需要注意的是Expires
和 cache-control
的 max-age
优先级高于 Expires
,如果它们同时出现,浏览器会优先使用 max-age
的值。
协商缓存
协商缓存
策略是每次请求都会发送请求,经过服务器端对资源的对比,来决定是使用本地缓存,还是新的资源。请求响应返回的 HTTP
状态为 304
,则表示缓存仍然有效。控制缓存的难题就是从浏览器端转移到了服务端。
控制协商缓存
有两种方式:
Last-Modified
和If-Modified-Since
ETag
和If-None-Match
Last-Modified 和 If-Modified-Since
服务端要判断缓存有没有过期,采取的方式是资源进行对比。如果浏览器直接把资源发送给服务端进行比对的话,网络开销太大,而且也会失去缓存的意义,所以显然是不可取的。
有一种简单的判断方法,是通过响应头部字段 Last-Modified
和请求头部字段 If-Modified-Since
比对双方资源的修改时间。
工作流程如下:
- 浏览器第一次请求资源,服务端在返回资源的响应头中加入
Last-Modified
字段,该字段表示这个资源在服务端上的最近修改时间。 - 当浏览器再次向服务端请求该资源时,请求头部带上之前服务端返回的修改时间,这个请求头叫
If-Modified-Since
- 服务端再次收到请求,根据请求头
If-Modified-Since
的值,判断相关资源是否有变化,如果没有,则返回304 Not Modified
,并且不返回资源内容,浏览器使用资源缓存值;否则正常返回资源内容,且更新Last-Modified
响应头内容。
这种方式虽然能判断缓存是否失效,但也存在两个问题:
- 精度问题,
Last-Modified
的时间精度为秒,如果在1
秒内发生修改,那么缓存判断可能会失效。 - 准度问题,如果一个文件被修改,然后又被还原,内容并没有发生变化,在这种情况下,浏览器的缓存还可以继续使用,但因为修改时间发生变化,也会重新返回重复的内容。
ETag 和 If-None-Match
为了解决精度问题和准度问题,HTTP
提供了另一种不依赖于修改时间,而依赖于文件哈希值的精确判断缓存的方式,那就是响应头部字段 ETag
和请求头部字段 If-None-Match
。
工作流程如下:
- 浏览器第一次请求资源,服务端会在响应头中加入
Etag
字段,Etag
字段值为该资源的哈希值; - 浏览器再次从服务端请求这个资源时,会在请求头上加上
If-None-Match
,值为之前响应头部字段ETag
的值; - 服务端再次收到请求,将请求头
If-None-Match
字段的值和响应资源的哈希值进行比对,如果两个值相同,则说明资源没有变化,返回304 Not Modified
;否则就正常返回资源内容,无论是否发生变化,都会将计算出的哈希值放入响应头部的ETag
字段中。
这种缓存比较的方式也会存在一些问题,具体表现在以下两个方面。
- 计算成本。生成哈希值相对于读取文件修改时间而言是一个开销比较大的操作,尤其是对于大文件而言。如果要精确计算则需读取完整的文件内容,如果从性能方面考虑,只读取文件部分内容,又容易判断出错。
- 计算误差。
HTTP
并没有规定哈希值的计算方法,不同服务端可能会采用不同的哈希值计算方式。这样带来的问题是,同一个资源,在两台服务端产生的Etag
可能是不相同的,所以对于使用服务器集群来处理请求的网站来说,使用Etag
的缓存命中率会有所降低。
需要注意:协商缓存
中,Etag
优先级比 Last-Modified
高。
ServiceWorker
Service workers
可以理解为充当 Web
应用程序、浏览器与网络(可用时)之间的代理服务器。主要目的是实现离线缓存,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API
。
使用方法
可以分为三步:
- 注册
- 安装
- 监听
在使用 ServiceWorker
脚本之前先要通过“注册”的方式加载它。
if ('serviceWorker' in window.navigator) {
window.navigator.serviceWorker
.register('./sw.js')
.then(console.log)
.catch(console.error)
} else {
console.warn('浏览器不支持 ServiceWorker!')
}
考虑到浏览器的兼容性,判断 window.navigator
中是否存在 serviceWorker
属性,然后通过调用这个属性的 register
函数来告诉浏览器 ServiceWorker
脚本的路径。
浏览器获取到 ServiceWorker
脚本之后会进行解析,解析完成会进行安装。可以通过监听 “install” 事件来监听安装,但这个事件只会在第一次加载脚本的时候触发。要让脚本能够监听浏览器的网络请求,还需要激活脚本。
在脚本被激活之后,我们就可以通过监听 fetch
事件来拦截请求并加载缓存的资源了。
下面是上面”注册“的sw.js
文件的内容:
const CACHE_NAME = 'ws'
// 这里是为什么是数组呢?因为们可以监听多个资源
let preloadUrls = ['/index.css']
/* 监听安装事件,install 事件一般是被用来设置你的浏览器的离线缓存逻辑 */
self.addEventListener('install', function (event) {
/* 通过这个方法可以防止缓存未完成,就关闭serviceWorker */
event.waitUntil(
/* 创建一个名叫 CACHE_NAME 的缓存版本 */
caches.open(CACHE_NAME)
.then(function (cache) {
/* 指定要缓存的内容,地址为相对于跟域名的访问路径 */
return cache.addAll(preloadUrls);
})
);
});
/* 注册 fetch 事件,拦截全站的请求 */
self.addEventListener('fetch', function (event) {
event.respondWith(
/* 在缓存中匹配对应请求资源直接返回 */
caches.match(event.request)
.then(function (response) {
// 匹配上直接返回缓存资源
if (response) {
return response;
}
return caches.open(CACHE_NAME).then(function (cache) {
const path = event.request.url.replace(self.location.origin, '')
return cache.add(path)
})
.catch(e => console.error(e))
})
);
})
这段代码首先监听 install
事件,在回调函数中调用了 event.waitUntil()
函数并传入了一个 Promise
对象。event.waitUntil
用来监听多个异步操作,包括缓存打开和添加缓存路径。如果其中一个操作失败,则整个 ServiceWorker
启动失败。
然后监听了 fetch
事件,在回调函数内部调用了函数 event.respondWith()
并传入了一个 Promise
对象,当捕获到 fetch
请求时,会直接返回 event.respondWith
函数中 Promise
对象的结果。
在这个 Promise
对象中,我们通过 caches.match
来和当前请求对象进行匹配,如果匹配上则直接返回匹配的缓存结果,否则返回该请求结果并缓存。
使用限制
- 在
ServiceWorker
中无法直接访问DOM
,但可以通过postMessage
接口发送的消息来与其控制的页面进行通信;
ServiceWorker
只能在本地环境下或HTTPS
网站中使用;ServiceWorker
有作用域的限制,一个ServiceWorker
脚本只能作用于当前路径及其子路径;- 由于
ServiceWorker
属于实验性功能,所以兼容性方面会存在一些问题。
总结
Expires
和cache-control
的max-age
优先级高于Expires
。强缓存
的优先级高于协商缓存
。协商缓存
中,Etag
优先级比Last-Modified
高。ServiceWorker
可以用来实现离线缓存,主要实现原理是拦截浏览器请求并返回缓存的资源文件。