前端缓存
1. 缓存定义
基本的网络请求的三个步骤:请求(前端) -> 处理(后端) -> 响应(前端)
缓存分类:
- 前端缓存(客户端缓存):在请求和响应中进行
- 在请求时,浏览器也可以通过存储结果直接使用资源,直接省去发送请求的步骤
- 在响应时,通过浏览器和服务器共同配合,减少响应内容来缩短传输时间
- 后端缓存(服务器缓存、数据库缓存):主要在处理中进行
- 在处理时,通过保留数据库连接、存储结果等缩短处理时间,尽快进入响应步骤
客户端缓存(浏览器缓存):
- 浏览器将符合条件的资源存储在客户端本地的一种存储机制
- 分类:
- http(协议)缓存:服务器端进行配置http协议相关字段
- web storage:存储数据
- app cache:应用程序缓存、离线缓存,实际效果不是很理想,比如更新资源后需二次更新、不支持增量更新、确认充足的容错机制
- indexedDB:存储数据
- file system api
服务器缓存(缓存服务器/代理缓存):
- 代理服务器接收多个客户端的数据请求,属于公有缓存,能减少多个客户端相同的资源请求,降低源服务器负载压力
缓存请求过程:
2. 前端缓存分类
前端缓存的优势:
- 减轻服务器压力
- 减少数据传输
- 节省网络带宽和流量
- 缩短页面加载时间
- 提升用户体验(缩短白屏体验)
前端缓存的缺点:
- 占据内存,因为部分缓存存储在内存当中
2.1. 按缓存位置分类
查看某个请求的最终的处理方式:chrome 开发者工具 -> network 栏 -> 请求的 size 字段,比如:
- from ServiceWorker
- from memory cache
- from disk cache
- 实际的数据大小,如 1.8M、298K
注意:请求的优先级是从 1 到 4 的,找到即返回,否则继续按后面的处理方式查找
浏览器请求资源时的过程:
- 调用 service worker 的 fetch 事件响应
- 查看 memory cache
- 查看 disk cache,若:
- 若有强制缓存且未失效,则使用强制缓存,不请求服务器(200)
- 若有强制缓存但已失效,则使用协商缓存,比较后确定是 200 还是 304
- 发送网络请求,等待网络响应
- 若 http 头部配置一些属于缓存的字段,则把响应存入 disk cache
- 把响应内容的引用存入 memory cache
- 若 service worker 的脚本调用了
cache.put()
,则把响应内容存入 service worker 的 cache storage
service worker:开发者编写的脚本,可以自行设置以进行灵活、直接的存储,操作的缓存有别于 memory cache 和 disk cache
位置:chrome 开发者工具 -> application 栏 -> cache storage 项
基本解释:
- service worker 的缓存是永久的,只有手动调用
cache.delete(resource)
或容量超过限制时,才会被浏览器清空 - 当未能命中 service worker 缓存,一般会使用 fetch()方法继续获取资源,此时就会去 memory cache、disk cache、实际网络请求中查找缓存,此时都会标注为 from serviceworker,根据情况,会把资源添加到缓存中,具体有:
- 根据 service worker 中的 handler 决定是否存入 cache storage
- 根据 http 头部的相关字段决定是否存入 disk cache
- memory cache 中会保存一份资源引用,备下次使用
memory cache:内存缓存,操作系统原理是先读内存,再读硬盘
基本解释:
- 几乎所有网络请求都会被浏览器自动加入到 memory cache 中,由于数量巨大且浏览器占用内存不能无限扩大,导致 memory cache 是个短期存储,其中几乎所有网络请求分为 2 块:
- preloader:浏览器打开网页时,会先请求资源,然后解析资源。preloader 的作用是一边解析执行资源(css/js),一边请求下一批资源(css/js)。这些请求过来的资源会被放入到 memory cache 中,供后面的解析执行操作使用
- preload:显示指定的资源预加载,会放入到 memory cache 中,比如
<link rel="preload"/>
- 一般情况下,浏览器关闭 tab 之后,该次浏览的 memory cache 便会失效(给其他 tab 腾出内存位置);当页面缓存占用极多的内存时,在 tab 关闭之前,最先保存的一些缓存就失效了
作用:
- memory cache 保证一个页面中多个相同的请求,实际上只会被请求一次,避免资源浪费
- memory cache 在匹配缓存时,必须保证 url 相同、类型相同、cors 域名规则相同,才能被认定为相同的请求
- 在从 memory cache 中获取缓存时,浏览器会忽略
max-age=0
(含义:不要在下次浏览时使用)、no-cache
等 http 头部配置,仍然从 memory cache 中读取。由于 memory cache 是短期使用,大部分情况生命周期仅是一次浏览,所以和max-age=0
不冲突 - 若不想让一个资源读取任何缓存,需设置
no-store
字段
disk cache:硬盘存储,存在文件系统中
基本解释:
- disk cache 允许相同资源在跨会话、跨站点时重复使用
- disk cache 严格根据 http 头部配置判断资源是否可缓存、是否可用、是否已失效需重新请求
- disk cache(属于持久性存储)会面临容量增长问题,所以浏览器会使用某种算法把古老的、可能过时的一个个资源删除
- disk cache 也叫 http cache,遵守 http 协议字段,所以一些 http 字段设定的缓存、强制缓存、协商缓存(对比缓存)均属于 disk cache
2.2. 按失效策略分类
按失效策略分为:
- 强制缓存
- 协商缓存
缓存处理流程:
- 资源响应阶段(无缓存/首次响应时):
- 资源二次请求阶段(再次使用资源时):
强制缓存:客户端请求时,会先访问缓存数据库看缓存是否存在,存在则直接返回;不存在则请求服务并将响应结果写入缓存
基本解释:
- 强制缓存直接减少请求数量,是提升最大的缓存策略
- 强制缓存的优化覆盖了请求的三个步骤,若考虑使用缓存优化网页性能,应首先考虑
- 可造成强制缓存的字段有:
- Expires(http/1.0):表示缓存到期时间,是一个绝对时间(当前时间+缓存时间)
- cache-control(http/1.1):表示资源缓存的最大有效时间,是一个相对时间,在该时间内,浏览器不向服务器发送请求
- cache-control 的优先级高于 expirs,为了兼容性,实际项目中两者都会设置
- cache-control 是相对时间,可保证服务器和客户端时间的一致性,并且他的可配置性也高
Expires:
- eg:
Expires: Thu, 10 Nov 2017 08:45:11 GMT
- 由于是绝对时间,若对本地时间进行修改,将导致浏览器判断缓存失效,而重新请求资源
- 若时区和其他误差导致的客户端和服务器之间的时间不一致,也可能导致缓存失效
- 写法十分规范,若不按照格式写,将会使设置失效
cache-control:
- eg:
Cache-control: max-age=2592000
- 定义:设置相对过期时间
- 常用属性值有:
- max-age:此文件在本地缓存的有效时间,到期时应该重新验证,但无方法确保该段时间内服务器文件不会修改,为了让浏览器下载最新的文件,可使用一些构建工具,例如 webpack,比如将 hash 值加入到文件中,此时对浏览器来说就是一个新的文件,就会从服务器获取新的,自然旧的缓存就没有了,适用于 css、js、图片
- must-revalidate:若超过了 max-age 设置的时间,浏览器必须发送请求,验证资源是否有效
- no-cache:不意味着没有缓存,只是告诉浏览器在使用缓存之前先验证服务器上的资源,到期时必须重新验证(类似于强制使用协商缓存);语义上表示下次请求时不要直接使用缓存而需要比对,并不对本地请求进行限制。故浏览器处理当前页面时,可以放心使用缓存;需要和 协商缓存字段 一起使用,浏览器将发送一个简单的请求验证文件状态,适用于 html
- no-store:真正的不要缓存,所有的都不缓存,每次请求资源时都会发送请求;对于一些十分重要的信息,比如提交的订单页面,使用这个可防止回退步骤重新提交
- public:所有内容都能缓存,包括客户端、代理(cdn)
- private:所有内容只有客户端才能缓存,代理服务器不能缓存。默认值
- 所有的属性值可以混合使用,以逗号隔开,混合使用时,优先级如下: 又: ![](../images/cache-control 指令使用.png)
协商缓存(对比缓存):当强制缓存失效时,就需要使用协商缓存,由服务器决定缓存内容是否失效
基本解释:
- 流程上,浏览器请求缓存数据库,返回一个缓存标识,之后浏览器拿该标识和服务器通讯。若缓存未失效(客户端返回给服务器的缓存标识 === 服务器存储的缓存标识),返回 304 表示可继续使用,若缓存失效,则返回新的数据和缓存规则,当浏览器响应后,再将规则写入缓存数据库中
- 协商缓存的请求数量和没有缓存时是一致的,但是优点是节省了响应体的体积(304 的情况下)
- 协商缓存的优化覆盖了请求三个步骤的响应步骤,通过减少响应体体积,来缩短网络传输时间;所以比强制缓存提升的幅度小
- 协商缓存可以和强制缓存一同出现,作为强制缓存的一种后备方案
- 协商缓存的两组字段:
- Last-Modified & If-Modified-Since
- ETag & If-None-Match
Last-Modified & If-Modified-Since:
- 流程:
- 服务器通过 Last-Modified 告诉客户端资源最后一次被修改的时间
- 浏览器将该值和内容一起记录到缓存中
- 下次请求相同资源时,浏览器从缓存中找出该缓存,在请求头中将上次的 Last-Modified 的值写入到 If-Modified-Since 字段中
- 服务器对比两个字段,若相等,则表示未修改(304),否则表示修改(200)
- 缺陷:
- 当资源更新速度频繁时,在秒以下的时间内进行修改,该缓存不能被使用,因为他只能精确到秒
- 某些服务器不能精确得到资源的最后修改时间,这样就无法通过最后修改时间判断资源是否得到更新
- 文件是通过服务器动态生成的,则更新时间永远是生成的时间,尽管文件内容没有任何变化,所以该缓存不能被使用
- last-modified 是一个弱缓存头信息,浏览器有自己的缓存策略,会自行决定是否从缓存中获取资源/下载新文件
- 若资源的最后修改时间改变了,但是内容没有改变,则会认为资源是修改过了的
- 加载图:
- 第一次加载
- 第二次加载-完美情况
- 第二次加载-通常情况
ETag & If-None-Match:
- ETag 存储的是文件的特殊标识(根据文件内容计算文件的特殊标识,若文件太大,则计算标识的时间也越长,生成模式有强验证和弱验证),服务器存储 ETag 字段,之后的流程和上者差不多,只不过是将 Last-Modified 换成了 ETag,其他类似
- ETag 的优先级高于 Last-Modified
- 加载图:
- 第一次加载
- 第二次加载
浏览器的行为:指用户在浏览器如何操作时,会触发何种策略,有:
- 打开网页,输入地址:查找 disk cache 是否有匹配,有则使用,无则请求
- 普通刷新(f5):因 tab 未关闭,会优先使用 memory cache
- 强制刷新(ctrl+f5):浏览器不使用缓存,故发送的请求头均有 no-cache,直接返回 200 和最新内容
3. 缓存的应用模式
模式 1:不常变化的资源:Cache-Control: max-age=31536000
- 通常在处理该类资源时,会给他们的 Cache-Control 配置一个很大的 max-age,这样在请求相同的资源时会命中强制缓存
- 为了解决更新的问题,须在文件名中使用 hash、版本号、query 参数等动态字符,达到修改引用 url 路径的目的,从而让之前的缓存失效(所以只要路径全称不同,则必然会请求新的资源,所以目前 electron 添加 fromtab 字段会影响 url 路径的改变)
模式 2:经常变化的资源:Cache-Control: no-cache
- 这类资源的特点是 url 不变化,但内容经常变化,所以可以设置 no-cache 字段迫使每次请求时都向服务器验证资源是否有效
- 这种模式,节省的是请求体的大小,优化效果不如模式 1
模式 3:两者结合(反例,非常危险):Cache-Control: max-age=600, must-revalidate
<=> Cache-Control: max-age=600
- 表面上资源 10 分钟内使用缓存,10 分钟后向服务器验证,实际暗藏风险,因为浏览器的缓存有自动清理机制、不同资源的请求时间也不尽相同
- 比如当有 3 种资源:index.html、index.css、index.js;在某次访问后,index.js 被缓存清理,此时浏览器配上新的 index.js+老的 index.html+老的 index.css 一起展现,可能会因为版本不同,导致错误
- 比如 a 请求 a.js、all.css,b 请求 b.js、all.css;若以 a -> b 的访问顺序访问,必然 all.js 的缓存时间早于 b.js,所以之后访问 b 时,可能会发生 all.css 被缓存请求,浏览器此时会是新的 all.css+老的 b.js,导致版本不同的错误
- 若要使用 max-age 做强制缓存,则不要设置一个太短的时间,较短的 max-age 的用处:适合整站使用合并后的 css 和 js(适合小型网站),否则可能会发生冗余,影响性能;适合资源是独立使用的,并不需要和其他文件配合生效的网站
4. 缓存的方案
方案 1:
- 使用构建工具(如 webpack)将唯一的 hash 添加到 css、js、图片路径上
- 对于 js、css、图片,设置:
Cache-Control: public, max-age=31536000
,不设置 ETag 和 Last-Modified - 对于 html,设置
Cache-Control: no-cache
和 ETag
方案 2:
- 不带hash的文件(例如index.html):使用协商缓存,设置
Cache-Control: no-cache
和 协商缓存字段,或不缓存 - 带有hash的文件(例如funcs.e3f472.js):使用强制缓存,设置
Cache-Control: max-age=31536000
和 ETag。文件重新打包后hash自动变化,会重新请求新的文件
5. 参考资料
- 一文读懂前端缓存 ,知乎,小蘑菇小哥
- 网站 cache control 最佳实践,知乎,安能
- 设计一个无懈可击的浏览器缓存方案:关于思路,细节,ServiceWorker,以及 HTTP/2,知乎,李熠, 和 1 类似
- 来,一起偷偷优化前端请求性能,然后惊艳所有人,知乎,大转转FE
- 从“快稳省安全”看Chromium——Chromium学习系列,知乎,姚文定,未看完
- 前端缓存方案,chrome search
- 详解web缓存,知乎,llyo
- 浅谈 Web 缓存,alloyteam,TAT.yana
- 谨慎处理 Service Worker 的更新,知乎,小蘑菇小哥
- 借助Service Worker和cacheStorage缓存及离线开发,张鑫旭
- 前端性能优化:配置ETag,未看完
- 前端部署 无感更新
- vue项目部署的最佳实践,开课吧技术团队,未看完