浏览器缓存的简单介绍和实践

date
May 14, 2022
slug
xdgqowvx
status
Published
tags
Web
summary
type
Post
 

前言

相对于以前的 html 开发阶段,现在的前端项目一般都会经过打包,然后再部署到服务器上,最后用户再访问。打包部署过程可玩性有很大提高,引入了前端工程管理的很多内容,比如多环境支持、动态配置、打包速度、访问加速等等。
今天这里只讨论资源文件的访问加速,C 端大概率是要用到 CDN 加速,B 端一般将资源放在服务器上即可,这两者都会涉及到浏览器缓存。
那如何利用浏览器缓存机制来实现我们的加速呢?就需要对浏览器缓存有一定理解。

浏览器缓存

浏览器缓存(Brower Caching)是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。
一是能减少服务器压力和数据传输,节省网站压力和带宽。二是能加快客户端的使用速度,提升系统使用体验。
浏览器缓存主要有两类:缓存协商和彻底缓存,也有称之为「协商缓存」和「强缓存」。
浏览器在第一次请求发生后,再次请求时:
  • 浏览器会先获取该资源缓存的 header 信息,根据其中的 Expires 和 Cache-control 判断是否命中强缓存,若命中则直接从缓存中获取资源,包括缓存的 header 信息, 本次请求不会与服务器进行通信
  • 如果没有命中强缓存,浏览器会发送请求到服务器,该请求会携带第一次请求返回的有关缓存的 header 字段信息(Last-Modified/IF-Modified-Since、Etag/IF-None-Match),由服务器根据请求中的相关 header 信息来对比结果是否命中协商缓存,若命中,则服务器返回新的响应 header 信息更新缓存中的对应 header 信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容。
是不是可以理解强缓存就是指从本地拿缓存,如果失败就去协商缓存?
浏览器缓存的位置有四种,当依次查找缓存且都没有命中的时候,才会去请求网络。更多介绍请看深入理解浏览器的缓存机制
  • Service Worker:开发者自己控制的缓存位置,控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
  • Memory Cache:一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
  • Disk Cache:是针对 Memory Cache 的补充,不同浏览器策略不一样。
  • Push Cache:是 HTTP2 中的内容,它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,

强缓存

与强缓存相关的 Header 字段是Cache-ControlExpires,这里重点介绍Cache-Control,它是 http1.1 出现的 header 信息。
它的值有以下几个:
  • max-age:重要!是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒。
  • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在 ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
  • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
  • public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
  • private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。
这下知道 no-cache 和 no-store 的区别了吧,面试时不要答错了。
Cache-Control 与 Expires可在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高,建议使用 Cache-Control 。
对于这两个 header,可以这么理解:

协商缓存

协商缓存可以理解为通过协商机制来实现缓存同步,这个机制的核心就是成双成对的响应头和请求头。协商步骤是:第一次请求资源时,浏览器会返回响应头和其值;再次请求资源时,浏览器会添加相应的请求头和上一次响应头中的值,服务根据这个值来判断是否命中缓存和后续不同的操作,被浏览器缓存的文件会有不同的缓存存储。所以分别用不同的字段来协商,它们关系如下:
  • 响应头(Last-Modify)/请求头(If-Modify-Since):其值是一个时间如 Thu,31 Dec 2037 23:59:59 GMT。
  • 响应头(ETag)/请求头(If-None-Match):其值是一个校验码。
下面说说二者的具体细节和差别。
1、If-Modified-Since 只可以用在 GET 或 HEAD 请求中。如果命中缓存则返回 304,并且不会返回资源内容,并且不会返回 Last-Modify。
2、ETag 响应头是资源的特定版本的标识符,可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。If-None-Match 是一个条件式请求首部,对于 GET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200。对于其他方法来说,当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候,才会对请求进行相应的处理。
3、If-None-Match 和 If-Modified-Since 一同使用的时候,前者优先级更高。If-None-Match 有以下优势:
  • 因为如果内容没有改变,Web 服务器不需要发送完整的响应,而如果内容发生了变化,使用 ETag 有助于防止资源的同时更新相互覆盖。
  • 与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于 ETag 重新生成过,response header 中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。
  • Last-Modified 标注的最后修改只能精确到秒级,1 秒内多次修改文件时就变得不可靠了。而 ETag 有多种生成的方法,比如资源内容的抗冲突散列函数生成的哈希值、最后修改时间戳的散列、仅使用资源的版本号。
到此大家明白了协商缓存的机制和细节了吧?

小结

那新的问题来了:当响应头既有 Last-Modified 又有 Etag,Etag 是多余的吗?
  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新请求;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说 1s 内修改了 N 次),If-Modified-Since 能检查到的粒度是秒级的,这种修改无法判断(或者说 UNIX 记录 MTIME 只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。 Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下才会继续比对 Last-Modified,最后才决定是否返回 304。
「强缓存与协商缓存的区别」
缓存类型
获取资源形式
状态码
发送请求到服务器
强缓存
从缓存取
200(from cache)
否,直接从缓存取
协商缓存
从缓存取
304(Not Modified)
否,通过服务器来告知缓存是否可用
「用户行为对缓存的影响」
用户操作
Expires/Cache-Control
Last-Modied/Etag
地址栏回车
有效
有效
页面链接跳转
有效
有效
新开窗口
有效
有效
前进回退
有效
有效
F5 刷新
无效
有效
Ctrl+F5 强制刷新
无效
无效

不缓存

no-store

上面提到强制缓存的 Cache-control 的指令 no-store ,作用是不使用任何缓存,配置为:Cache-Control: no-store
需要注意,Cache-Control 是通用消息头字段,既可以用于请求头,也可以用于响应头。
这里额外引用几个 MDN 里示例,说明下其他场景该如何配置。
1、缓存静态资源:Cache-Control:public, max-age=31536000,对于应用程序中不会改变的文件,你通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,CSS 文件和 JavaScript 文件。
2、需要重新验证:Cache-Control: no-cacheCache-Control: max-age=0, must-revalidate,表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。————可理解更改默认的缓存机制,将「强缓存 → 协商缓存」改为「协商缓存 → 强缓存」。
3、注意在服务器关闭或失去连接,Cache-Control: max-age=0可能会使用本地缓存。

增加版本号

这种方法不需要依赖服务端,纯前端便可实现。该方法流行于前端工程化诞生之前,弊端是需要手动增加版本号,人为干预较多。
<script type="text/javascript" src="../js/jquery.min.js?version=1.7.2" ></script>

使用随机数

既然在文件后面添加指纹可以让浏览器重新获取资源,那么我们可以在后面拼接随机数或者时间戳,这样也可以达到相同的目的,还省去了手动更改版本号的步骤。
具体来说,可以在 index.html 增加一段脚本,用来动态生成一个 script 标签,并引入静态资源,拼接时间戳。
这样浏览器每次刷新后,便会动态生成一个包含时间戳的静态资源。浏览器发现文件名有更改,会重新获取静态资源,达到了不缓存文件的目的。
var script = document.createElement("script");
script.src = "/resource/options/myjs.js?randomId=" + new Date().getTime();
document.body.appendChild(script);
其实现在打包工具增加 hash 值就是利用此种思路。

使用 HTML 禁用缓存

HTML 也可以禁用缓存, 即在页面的 head 标签中加入 meta 标签。例:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
说明:虽能禁用缓存,但只有部分浏览器支持,而且由于代理不解析 HTML 文档,故代理服务器也不支持这种方式。该方法不适用于特定文件不缓存的要求。

实践

下面针对工程化的前端项目,纯前端工程可自行参考修改:
1、入口文件 html 如 index.html,使用Cache-Control: no-store,nginx 配置参考
location / {
  index  index.html;
  try_files $uri $uri/ /index.html;

  # 针对入口文件禁用缓存
  if ($request_filename ~* .*\.(htm|html)$)
  {
     add_header cache-control no-store;
  }
}
2、打包后的资源文件名增加 hash 值方便 CDN 部署,如果是自己服务器则设置较长的缓存时间Cache-Control:public, max-age=31536000,nginx 配置参考
if ($request_filename ~* .*\.(?:js|css)$)
{
  add_header cache-control public, max-age=31536000;
}
3、针对如 public 下的静态资源,因不会参与打包过程,所以建议在引用时增加版本号或时间戳型随机数。
如果是 webpack 可使用html-webpack-include-assets-plugin插件,开启addHash选项。

参考资料:
 

© 刘德华 2020 - 2023