本文翻译自:jakearchibald.com/2016/cachin…javascript
这是一篇2016年的老文章。做者是Chrome浏览器的开发成员。css
本文首发于公众号:符合预期的CoyPanjava
使用正确的缓存能够带来巨大的页面性能上的收益,节省带宽,减小服务器成本。可是许多网站并无解决好他们的缓存问题,创造了一个race conditions,致使相互依赖的资源之间失去了同步。web
绝大多数缓存的最佳实践,都属于下面两种模式:gulp
Cache-Control: max-age = 31536000
复制代码
在这种模式下,你不会去改变特定url下的文件内容,你直接改变url:浏览器
<script src="/script-f93bca2c.js"></script>
<link rel="stylesheet" href="/styles-a837cb1e.css">
<img src="/cats-0e9a2ef4.jpg" alt="…"> 复制代码
每个URL都包含一个跟随文件内容变换的部分。这个部分能够是版本号,修改日期,或者文件内容的hash值。缓存
大多数服务端框架都有工具能够简单的实现这个需求。Node.js下还有更轻量级的工具可以作到一样的事情,好比gulp-rev.安全
可是,这种模式不适合诸如文章、博客这样的场景。文章和博客的URL是不会有版本号的,并且他们的内容可以随时修改。说真的,若是我在文章中犯了拼写或者语法错误,那么我须要可以快速、频繁的修改文章内容。bash
Cache-Control: no-cache
复制代码
注意:
no-cache
并不意味着不缓存,而是使用缓存前必须请求服务端进行检查(或者说叫从新校验)。no-store
告诉浏览器,根本不要缓存这个文件。同时,must-revalidate
也不是说就『must-revalidate』,而是若是本地资源的缓存时间尚未超过设置的max-age的值,就能够直接使用本地资源,不然必须从新校验。服务器
在这种模式下,你能够在响应头里添加一个ETag(你选择的版本ID)或者Last-Modified。客户端下一次请求资源时,会分别带上If-None-Match和If-Modified-Since,服务端会判断说:直接使用你已有的本地资源吧,他们是最新的。这就是最多见的:HTTP 304
若是没有带上ETag/Last-Modified,服务端会再次返回完成的内容。
这种模式老是会发起一个网络请求,而模式一是能够不用经过网络的。
使用模式一时,由于网络基础建设而致使的延时是很常见的,使用模式二时,也很容易遇到网络环境带来的延迟。取而代之的是中间的东西:一个短期的max-age设置和可变的内容。这是一种十分糟糕的妥协。
不幸的是,这种作法并不是不常见。好比,Github pages就是这样的。
想象一下有如下三个url:
服务端都是返回的:
Cache-Control: must-revalidate, max-age=600
复制代码
这种模式在测试的时候看起来是能够的,但在现实中,会出问题,而且很难追踪。在上面的例子中,服务端确实已经更新了HTML, CSS 和JS,可是页面最终使用了缓存里的HTML,JS,CSS倒是从服务端获取的最新的版本。资源版本不匹配致使了页面出错。
一般状况下,当咱们对HTML进行重大更改时,咱们还可能更改HTML对应的CSS结构,并更新JS以适应样式和内容的更改。这些资源是相互依赖的,可是缓存的header是没法描述这种依赖的。用户最终看到的,多是一两个新版本的资源,和其余老的资源。
max-age和响应时间有关,所以,若是上述全部的资源都是在同一次访问中请求的,他们大概会在同一时间到期,可是仍然有很小的可能发生竞争。若是你的某些页面并不包含JS或者包含了不一样的CSS,那么过时时间可能就不一样步了。更糟糕的是,更糟糕的是,浏览器老是从缓存中删除东西,它不知道HTML、CSS和JS是相互依赖的,因此它会很高兴地删除一个而不是其余的。上述的状况,均可能会致使页面资源的版本不匹配。
对用户来讲,他们最终会看到错误的页面布局和错误的页面功能,从细微的错误到彻底不可用的内容。
谢天谢地,对用户来讲仍是有补救措施的。
若是页面做为刷新的一部分加载,浏览器会忽略max-age,向服务器进行验证。所以,若是用户遭遇了由于max-age而形成的错误,刷新是能够解决问题的。固然,强迫用户这样作会下降信任度,由于这会让你感受到你的网站是不靠谱的。
假设你有如下的service worker:
const version = '2';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
'/styles.css',
'/script.js'
]))
);
});
self.addEventListener('activate', event => {
// …delete old caches…
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
复制代码
这个service-worker
若是咱们更改了CSS/JS,咱们会修改service-worker中的版本号,触发service-worker的更新。可是,假如addAll发出的请求通过了HTTP缓存(和其余大多数缓存同样),咱们也会进入到max-age的race condition,缓存不匹配的CSS、JS版本。
一旦他们被缓存了,咱们将会一直看到不匹配的CSS和JS,直到咱们下一次更新service-worker。而在下一次更新时,咱们可能还会陷入另外一个race condition。
你能够在service worker中跳过缓存:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
new Request('/styles.css', { cache: 'no-cache' }),
new Request('/script.js', { cache: 'no-cache' })
]))
);
});
复制代码
不幸的是,这个缓存的设置在Chrome/Opera中还不支持,Firefox也是刚刚支持。你能够本身来实现相似的功能:
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => Promise.all(
[
'/styles.css',
'/script.js'
].map(url => {
// cache-bust using a random query string
return fetch(`${url}?${Math.random()}`).then(response => {
// fail on 404, 500 etc
if (!response.ok) throw Error('Not ok');
return cache.put(url, response);
})
})
))
);
});
复制代码
在上述代码中,我用随机数来避免缓存,可是你能够更进一步,在构建的时候为内容增长一个hash值(和sw-precache作的事差很少)。这是一种在js层面的对模式一的实现,可是仅仅对service worker的使用者是有效的,而不是对全部的浏览器和你的CDN都有效。
正如你所见,你能够绕过service worker中糟糕的缓存,可是你最好解决根源的问题。正确的设置缓存可以让你在使用service worker的时候更加轻松,而且对那些不支持service worker的浏览器也是有好处的,还能让你充分的使用你的CDN。
正确的缓存头还意味着你能够大量简化server worker的更新:
const version = '23';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`static-${version}`)
.then(cache => cache.addAll([
'/',
'/script-f93bca2c.js',
'/styles-a837cb1e.css',
'/cats-0e9a2ef4.jpg'
]))
);
});
复制代码
在这里,我将使用模式2(服务器从新验证)缓存根页面,其他资源使用模式1(不可变内容)。每次service worker更新都将触发对根页面的请求,但只有当资源的URL发生更改时,才会下载其他资源。这很好,由于不管你是从之前的版本仍是第10个版本更新,它均可以节省带宽并提升性能。
相对于本地应用来讲,这是一个巨大的优点。在本地应用中,无论二进制内容有细微和巨大的改变,整个二进制内容都会被下载。而在这里,咱们只须要一个小小的下载,就能更新巨大的web app.
service worker的工做最好是做为一个加强方案,而不是变通方案。因此预期与缓存抗争,不如好好利用缓存。
对于可变内容使用max-age通常状况下是一个错误的选择,但也不老是这样。好比,这个页面设置了一个3分钟的max-age. race condition在这个页面是不会成为问题的,由于这个页面没有任何遵循这一种模式的依赖(个人css,js,图片等都遵循模式1-不可变内容),依赖于此页的任何内容都不会遵循相同的模式。
这种模式意味着,若是我有幸写了一篇热门文章,个人cdn可让个人服务器散热,而我能忍受用户须要花三分钟时间才看到文章更新。
这种模式不能随便使用。若是我在文章中添加了一个新的部分,而且将这个部分连接到一篇新的文章,那么我就创造了一个会争用的依赖项。用户能够单击连接,并在没有引用部分的状况下获取文章的副本。若是我想避免这种状况,我就得更新第一篇文章,刷新cdn, 等待3分钟,而后在另外一篇文章中添加指向他的连接。是的…..你必须很是当心这种模式。
正确使用,缓存能极大的提升性能而且较少带宽消耗。对于任何容易更改的URL,都支持不可变的内容,不然在服务器从新验证时会使其安全。只有当你足够勇敢,而且你确信你没有可能会失去同步的依赖项时,再使用max-age和可变内容的模式。