使用缓存会带来巨大的性能提升,还能节省带宽、减少服务端开销,但很多网站对缓存一知半解,让相互依赖的资源出现竞态条件,从而无法同步更新。
使用缓存的最佳实践大体上可以归纳为这两种模式:
模式一:不变内容 + 长时间 max-age
Cache-Control: max-age=31536000
- URL 对应的内容绝对不会改变,因此:
- 浏览器 / CDN 直接将这个资源缓存一年也没问题;
- 在 max-age指定时间内,缓存副本可以直接使用,不需要与服务端协商;
页面:嘿,我需要 "/script-v1.js"、"/styles-v1.css" 和 "/cats-v1.jpg"。(10:24)
缓存:我这儿都没有。 服务端,你有么?(10:24)
服务端:当然,给你。对了 缓存,把这些资源存起来,一年之内直接用吧。(10:25)
缓存:多谢!(10:25)
页面:赞哦!(10:25)
第二天
页面:嘿,这次我要 "/script- v2.js"、"/styles- v2.css" 和 "/cats-v1.jpg"。(08:14)
缓存:我只有其中一个,先拿去用,剩下的我没有。 服务端,看看?
服务端:好,给你新的 CSS 和 JS 文件。对了 缓存,这些你也留着用一年吧。(08:15)
缓存:超赞!(08:15)
页面:多谢!(08:15)
后来
缓存:嗯,"/script-v1.js" 和 "/styles-v1.css" 有一阵子没被用到。我决定删掉它们。(12:32)
在这种模式中,绝对不会修改某个 URL 的内容,只会改变 URL 本身:
<script src="/script-f93bca2c.js"><link rel="stylesheet" href="/styles-a837cb1e.css"><img src="/static/imghwm/default1.png" data-src="/cats-0e9a2ef4.jpg" class="lazy" alt="…">
上面每个 URL 都包含了与文件内容同步修改的部分。这部分可以是版本号、修改时间,或者是文件内容的 MD5 —— 我的博客就是这么干的。
大部分服务端框架都有对应工具来轻松完成这项工作(我在用 Django 的 ManifestStaticFilesStorage),还有一些很轻量的 Node.js 库可以实现同样功能,例如 gulp-rev。
但是,文章或博客详情这类 HTML 页面不适用于这种模式。这些页面 URL 不能包含版本号,页面内容还必须能修改。尤其是发现有拼写错误或语法错误后,需要快速频繁地更新。
模式二:可变内容,每次都走服务端验证
Cache-Control: no-cache
- URL 对应的内容可能会改变,因此:
- 没有服务端的指示,不能使用本地缓存的任何版本;
页面:嘿,我需要 "/about/" 和 "/sw.js" 两份资源。(11:32)
缓存:我搞不定。 服务端,看看?(11:32)
服务端:哈,我有,给你。 缓存,你可以自己存一份,但用之前要先问我哈。(11:33)
缓存:明白!(11:33)
页面:多谢!(11:33)
第二天
页面:我又想要 "/about/" 和 "/sw.js"。(9:46)
缓存:等一下。 服务端,我能直接用我本地的副本吗?在我这儿,"/about/" 最后修改于周一,"/sw.js" 最后修改于昨天。(9:46)
服务端:"/sw.js" 在那之后就没改过呢。(9:47)
缓存:赞! 页面,给你 "/sw.js"。(9:47)
服务端:但 "/about/" 改过,我给你最新的版本。 缓存,跟之前一样,你可以自己存一份,但用之前要先问我~(9:47)
缓存:了解!(9:47)
页面:赞噢!(9:47)
注: no-cache并不是说「不缓存」,它意味着使用缓存前必须检查(或者说 验证)这个资源在服务端是否有更新。 no-store用来告知浏览器完全不要缓存这个资源。类似的, must-revalidate并不是说「每次都要验证」,它意味着某个资源在本地已缓存时长短于 max-age指定时长时,可以直接使用,否则就要发起验证。好,这下明白了。
这种模式下,也可以给资源响应加上 ETag(资源的版本 ID)、 Last-Modified时间这两个头部。下次客户端请求这些资源时,会通过 If-None-Match或 If-Modified-Since这两个请求头带上之前的值,这样服务端就可以返回「直接用你之前缓存的版本吧,它们是最新的」,换成行话就是「HTTP 304」。
如果服务端没办法发送 ETag/ Last-Modified头部,那每次都需要发送完整的响应内容。
这种模式下,每次都会产生网络请求,所以它没有能节省网络请求的模式一好。
模式一被基础设施影响,模式二被网络请求影响,都是常见的事儿。所以又有了中间方案:给可变内容加上短一点的 max-age。这个折中方案太太太糟糕了。
给可变内容加上 max-age,通常是错的
不幸的是这种做法并不罕见,Github pages 当前就是这样。
假设这样三个资源:
- /article/
- /styles.css
- /script.js
都有这样的响应头:
Cache-Control: must-revalidate, max-age=600
- URL 对应的内容发生了改变;
- 如果浏览器有一个十分钟之内的缓存副本,就不会与服务端协商,直接使用;
- 否则发起网络请求,可能的话还会带上 If-Modified-Since和 If-None-Match请求头;
页面:嘿,我需要 "/article/"、"/script.js" 和 "/styles.css"。(10:21)
缓存:我这里没有, 服务端?(10:21)
服务端:没问题,给你。对了 缓存,这些资源你可以存 10 分钟。(10:22)
缓存:明白!(10:22)
页面:多谢!(10:22)
6 分钟后
页面:嘿,我又想要 "/article/"、"/script.js" 和 "/styles.css"。(10:28)
缓存:天哪,真抱歉,我把 "/styles.css" 给弄丢了,其它的都有,先给你。 服务端,你能再把 "/style.css" 发给我吗?(10:28)
服务端:当然可以,实际上它在你上次请求之后发生了改变。同样,你又可以把它缓存 10 分钟。(10:29)
缓存:没问题。(10:29)
页面:多谢!等等!彻底挂了!!发生什么啦?(10:29)
这种场景在测试环境可以构造出来,但在真实环境中难复现,也难追查。在上述例子中,实际上服务端同时更新了 HTML、CSS 和 JS,但是页面最终从缓存中拿到旧的 HTML 和 JS,并从服务端拿到最新的 CSS。版本不匹配导致功能异常。
通常,当我们对 HTML 改动很大时,很可能 CSS 需要为新结构作出调整,JS 也需要配合 CSS 和 HTML 改动而进行相应修改。这些资源相互依赖,但无法通过缓存头反映出来。最终页面可能会拿到一部分新资源,一部分旧资源。
max-age是响应时间的相对值,某个页面上的所有资源请求,会被设置在大致相同的时间后失效,但仍有小概率出现竞争。如果你有一些不包含 JS、或包含不同 CSS 的页面,过期时间可以不同步。更为糟糕的是,浏览器一直都在淘汰缓存的资源,它不可能知道某些 HTML、CSS 和 JS 相互有依赖,所以会出现部分淘汰的情况。综上所述,最终页面拿到版本不匹配的资源并非不可能发生。
对于用来来说,这会破坏页面布局和/或功能,从小问题到大事故都有可能发生。
谢天谢地,我们有个解决方案。。。
刷新之后通常就好了
刷新页面,会让浏览器向服务端发起验证,忽略 max-age。所以如果用户对 max-age的这个问题很有经验的话,点击刷新按钮就能解决一切问题。当然,要求用户这样做会降低用户对你的信任,会让用户觉得你的网站很不稳定。
service worker 会延长这个 BUG 的生命周期
假设有下面这样的 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 时,我们需要同步修改 version,用来让 service worker 缓存失效,触发更新。然而,因为 addAll会从 HTTP 缓存中获取资源(跟其它请求一样),我们又有可能遇上 max-age竞态条件,从而缓存相互不兼容的 CSS 和 JS 版本。
而一旦它们被缓存,意味着直到下次更新 service worker 之前,页面都会访问到不兼容的 CSS 和 JS —— 这还是假设下次更新不出现竞争的情况。
你也可以在 service worker 里绕过 HTTP 缓存:
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都不支持 cache选项,只有 最新的 Firefox Nightly才支持,当然你也可以自己解决:
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); }) }) )) );});
上面代码通过加随机数的方式绕过了缓存,也可以更进一步,利用构建工具自动添加文件内容 MD5(类似于 sw-precache所做的工作)。这有点像在 JavaScript 中实现了模式一,但是只能让 service worker 受益,不包括浏览器和 CDN。
让 service worker 和 HTTP 缓存相互协作,而不是打架
如你所见,我们可以用一些技巧来改善 service worker 中的缓存,但更好的做法是从源头解决问题。正确使用 HTTP 缓存不但可以简化 service worker 逻辑,还可以让那些不支持 service worker 的浏览器获益(Safari、IE/Edge),也能用好 CDN。
正确配置缓存响应头意味着可以大幅简化 service 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' ])) );});
在这个例子中,我使用模式二(可变内容,每次都走服务端验证)缓存 HTML 页面;其它资源使用了模式一(不变内容 + 长时间 max-age)缓存。每次 service worker 更新都会触发 HTML 页面请求,而其它资源只有在 URL 发生变化时才会再次下载。这非常棒,无论是从上个版本还是上十个版本更新都能节省带宽、提高性能。
相比有细微改动整个二进制文件就要重新下载,或者要实现复杂的二进制 diff 的原生应用,这是一个巨大的优点。只需要少量下载,就可以更新大型 Web 应用。
service worker 最好用于局部增强而不是提供整套方案,它应该与 HTTP 缓存配合使用,而不是相互打架。
谨慎地给可变内容加上 max-age,能带来受益
给经常改变的内容设置 max-age通常是错误的选择,但也不全是。现在你看到的这个页面(译者注:指原文页面)就设置了三分钟的 max-age。在这里竞态条件不是问题,因为这个页面不依赖任何使用同样缓存模式的资源(我的 CSS、JS 及图片都属于模式一:不变内容),也不被其它使用同样缓存模式的页面所依赖。
这种模式意味着,如果我足够幸运写了一篇大受欢迎的文章,我的 CDN(Cloudflare)会帮我的服务器抗住流量,只要我能接受修改文章要等三分钟才能被用户看到,我确实可以接受。
这种模式用起来也没那么容易。如果我给一篇文章增加了一段内容,再在另外一篇文章中指向它,我就在页面之间创建了会引入竞争的依赖关系。用户可能点击链接后看到的是不包含新增内容的缓存副本。要避免这种问题发生,我必须在更新文章后,去 Cloudflare 的控制台刷新这篇文章的缓存,等三分钟再在其它文章加上指向它的链接。是的,你必须非常小心地使用这种模式。
只要用法恰当,缓存能极大的提升性能、节省带宽。让不变内容可以轻松改变 URL,让可变内容走服务端验证。如果你很勇敢,当你能确认你的内容既不依赖别人也不被别人依赖时,才针对可变内容使用 max-age,因为它可能无法同步更新。

HTML是构建网页结构的基石。1.HTML定义内容结构和语义,使用、、等标签。2.提供语义化标记,如、、等,提升SEO效果。3.通过标签实现用户交互,需注意表单验证。4.使用、等高级元素结合JavaScript实现动态效果。5.常见错误包括标签未闭合和属性值未加引号,需使用验证工具。6.优化策略包括减少HTTP请求、压缩HTML、使用语义化标签等。

HTML是一种用于构建网页的语言,通过标签和属性定义网页结构和内容。1)HTML通过标签组织文档结构,如、。2)浏览器解析HTML构建DOM并渲染网页。3)HTML5的新特性如、、增强了多媒体功能。4)常见错误包括标签未闭合和属性值未加引号。5)优化建议包括使用语义化标签和减少文件大小。

WebDevelovermentReliesonHtml,CSS和JavaScript:1)HTMLStructuresContent,2)CSSStyleSIT和3)JavaScriptAddSstractivity,形成thebasisofmodernWebemodernWebExexperiences。

HTML的作用是通过标签和属性定义网页的结构和内容。1.HTML通过到、等标签组织内容,使其易于阅读和理解。2.使用语义化标签如、等增强可访问性和SEO。3.优化HTML代码可以提高网页加载速度和用户体验。

htmlisaspecifictypefodyfocusedonstructuringwebcontent,而“代码” badlyLyCludEslanguagesLikeLikejavascriptandPytyPythonForFunctionality.1)htmldefineswebpagertuctureduseTags.2)“代码”代码“ code” code code code codeSpassSesseseseseseseseAwiderRangeLangeLangeforLageforLogageforLogicIctInterract

HTML、CSS和JavaScript是Web开发的三大支柱。1.HTML定义网页结构,使用标签如、等。2.CSS控制网页样式,使用选择器和属性如color、font-size等。3.JavaScript实现动态效果和交互,通过事件监听和DOM操作。

HTML定义网页结构,CSS负责样式和布局,JavaScript赋予动态交互。三者在网页开发中各司其职,共同构建丰富多彩的网站。

HTML适合初学者学习,因为它简单易学且能快速看到成果。1)HTML的学习曲线平缓,易于上手。2)只需掌握基本标签即可开始创建网页。3)灵活性高,可与CSS和JavaScript结合使用。4)丰富的学习资源和现代工具支持学习过程。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

SublimeText3 Linux新版
SublimeText3 Linux最新版

EditPlus 中文破解版
体积小,语法高亮,不支持代码提示功能

PhpStorm Mac 版本
最新(2018.2.1 )专业的PHP集成开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

记事本++7.3.1
好用且免费的代码编辑器