前端缓存:性能的无声杀手|技术创作特训营第一期

2023-08-27 13:27:11 浏览数 (2)

当今互联网时代,用户对网站性能和加载速度的要求越来越高。作为前端开发人员,了解和实施适当的缓存策略是确保优质用户体验的重要一环。想象一下,您正在访问一个网站,页面加载缓慢,图片无法显示,样式丢失,这种情况不仅影响了您的心情,也可能让您转而寻找其它更快速稳定的同类型网站。

缓存作为一种性能优化技术,可以显著提高网站的加载速度,降低服务器负载,同时也节省了用户的时间和流量。然而,前端缓存并非一蹴而就的简单解决方案,而是涉及多个复杂的策略和机制。

01 基本概念

关于缓存,百度百科是这么解释的——缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。 缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。

我们要关注的缓存非彼缓存,与前端缓存有本质性的区别,但共同的地方是访问速度快、性能高。那么,什么是前端缓存呢?大家都知道前端开发离不开网络和浏览器,前端缓存也可以直接看作是HTTP缓存和浏览器缓存的结合,属于相辅相成的关系。

HTTP 缓存是产生于客户端与服务器之间通信的一种缓存,利用这一缓存可以提升服务器资源的重复利用率,在有效的时间内不必每次都向服务器请求相同的资源,大大减少服务器的压力;而浏览器缓存则是浏览器提供的一种缓存机制,可以将服务器资源和网页访问产生的临时数据缓存到内存或本地,提升客户端的加载速度。

前端缓存有多种,不同的分类方式结果可能是不同的,我们通过下图来把缓存进行分类:

前端缓存分类前端缓存分类

02 HTTP缓存

我们先了解一下HTTP的含义:超文本传输协议(英语:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议,是因特网上应用最为广泛的一种网络传输协议。简单来说就是一种发布和接收HTML 页面的方法,被用于在Web 浏览器和网站服务器之间传递信息。

从概念上我们知道,HTTP协议用于客户端和服务端之间的通信,请求由客户端发出,服务端响应请求。

2.1 初探缓存的请求响应

HTTP请求主要发生在客户端,请求是由报文的形式发送的,请求报文由三部分组成:请求行、请求报头和请求正文。HTTP响应报文也由三部分组成:状态行、响应报头和响应正文。

我们打开浏览器(以chrome为例)看下与缓存有关的请求报头和响应报头:

我们可以看到报头是由一系列中间用冒号 “:” 分隔的键值对组成,我们把它称为首部字段,其由首部字段名和字段值构成。

如:Content-Type: application/json;charset=utf-8

以上首部字段名为 Content-Type,首部字段值为 application/json;charset=utf-8,表示报文主体的对象类型。

首部字段分为四种类型:

  • 通用首部字段(请求报头和响应报头都用到的首部,例如:Cache-Control、Date等
  • 请求首部字段(请求报头用到的首部,例如:User-Agent、Accept等
  • 响应首部字段(响应报头用到的首部,例如:Content-Type、Set-Cookie等
  • 实体首部字段(针对请求报头和响应报头实体部分使用的首部,例如:Content-Length、Content-Language等

与缓存有关的首部字段名

第一节我们提到HTTP 缓存可以拆解为强缓存协商缓存,为了便于理解记忆,使用思维导图来进行展示:

2.2 揭开缓存字段的神秘面纱

上一小节跟强缓存相关的首部字段名主要有两个:Expires Cache-Control,我们依次来进行解释说明。

Expires

Expires 首部字段是HTTP/1.0中定义缓存的字段,其给出了缓存过期的绝对时间,即在此时间之后,响应资源过期,属于实体首部字段

示例: Expires: Wed, 11 May 2023 12:50:47 GMT

表示该资源将在以上时间之后过期,在该时间前浏览器有权直接从浏览器缓存中读取数据,不必再向服务器发送请求。

注意:不必再向服务器发送请求是因为命中了强缓存。

但是因为 Expires 设置的缓存过期时间是一个绝对时间,所以会受客户端时间的影响而变得不精准。

Cache-Control

Cache-Control 首部字段是HTTP/1.1中定义缓存的字段,用于控制缓存行为,可以组合使用多种指令,多个指令之间可以通过 “,” 分隔,属于通用首部字段。常用的指令有:max-age、s-maxage、public/private、no-cache/no store 等。

示例:

Cache-Control: max-age:3600, s-maxage=3600, public

Cache-Control: no-cache

max-age 指令可以设置缓存过期的相对时间,单位为秒数。跟 Expires 同时出现时,max-age 的优先级更高。一般为了向下兼容,两者都会经常出现在响应首部中。同时 max-age 还可在请求首部中被使用,告知服务器客户端希望接收一个存在时间不大于多少秒的资源。

s-maxage 指令与max-age的区别在于,s-maxage只适用于公共缓存服务器,例如请求资源从源服务器发出后又被中间的代理服务器接收缓存。

注意:使用 s-maxage 指令后,代理服务器将忽略 Expires 和 max-age 指令的值。

public 指令表示所请求的资源可以被所有节点缓存,其中包括客户端和代理服务器,那么与之对应的 private 指令则表示资源只允许客户端缓存,其他节点代理服务器不会进行缓存。因此,设置了 private 指令后 s-maxage 指令将被忽略。

然后是 no-cache no-store 指令,它们都是HTTP头部指令,用于控制缓存行为,但有不同的含义,no-cache 指示客户端不应直接从缓存中提供响应,而应先发送一个验证请求到服务器以确认响应是否仍然有效。服务器可以根据实际情况决定是否发送实际的响应内容,或者只返回一个 304 Not Modified 响应来指示客户端使用缓存副本,比较适合一些身份校验频繁的场景;no-store 指示客户端不应将响应存储在任何缓存中,客户端不会保留响应的副本,每次需要响应时都必须向服务器发出请求,可以用于确保敏感数据不会被存储在客户端设备上的场景。

我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部,如下示例:

<meta http-equiv="Cache-Control" content="no-cache" />

Last-Modified 与 If-Modified-Since

Last-Modified 属于响应首部字段,表示资源最后修改时间,当浏览器首次获取到服务器返回资源的Last-Modified值后,会把这个值存储起来,等到下次访问该资源时通过携带 If-Modified-Since 请求首部发送给服务器验证该资源有没有过期。

示例:

Last-Modified: Fri , 14 May 2021 17:23:13 GMT

If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT

客户端向服务器发送If-Modified-Since时,它实际上在询问服务器该资源是否已经被修改。服务器将比较客户端提供的 If-Modified-Since 值与当前资源的 Last-Modified 值,如果该资源没有被修改,服务器将返回一个 304 Not Modified 响应,表示客户端的缓存仍然有效,客户端可以使用已缓存资源。如果资源已经被修改,服务器将返回新的资源,状态码为200,并返回新的Last-Modified 值。

Etag 与 If-None-Match

Etag 属于响应首部字段,代表资源的唯一性标识,服务器会按照指定的规则生成资源的标识。当资源发生变化时,Etag 的标识也会更新。当浏览器第一次接收到服务器返回资源的 Etag 值后,其会把这个值存储起来,并在下次访问该资源时通过携带 If-None-Match 请求首部发送给服务器验证该资源有没有过期。

示例:

Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

If-None-Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

如果服务器校验 If-None-Match 值与 Etag 不一致,说明服务器上的文件已被更新,那么服务器会发送更新后的资源给浏览器并返回最新的 Etag 值,浏览器收到资源后会更新缓存的 If-None-Match 的值。

注意:Etag 能够检测文件内容是否发生变化,而Last-Modified只能知道是否更新过文件,即使内容没变化也算做一次更新,Etag 优先级高于 Last-Modified。

2.3 强缓存

前面章节有涉及到强缓存,但没有展开详细介绍,在这一小节,我们一起了解一下。

我们先说一个场景,大家应该都有遇到过,我们在第一次访问网站的时候打开速度会有点慢,再次访问的时候速度就快了很多,这个是比较常见的,其实这个背后主要是强缓存的作用。

我们先禁用缓存模拟看一下第一次打开某东的首页,右键检查打开开发者工具到network看一下资源加载。

我们重点关注下 Size Time 列的数据,Size 列表示浏览器从服务器获取资源的大小,Time 列表示资源加载耗时。因为几乎每一个资源都需要从服务器获取并加载,所以网页打开速度会受到影响,这里浏览器用了1.05s加载完了页面的所有资源(图片、脚本、样式等),1.7 MB 的数据被传输到了本地。

从强缓存的角度来看,其实第一次访问网页时浏览器已经开始在背后进行强缓存的判断和处理,我们可通过以下流程图一探究竟:

在浏览器发起HTTP请求时,会去查询浏览器缓存有没有该资源的缓存数据,如果没有则向服务器发起请求,服务器接受请求将资源返回给浏览器,浏览器会将资源的响应数据存储到浏览器缓存,这就是强缓存的过程。

下面,我们第二次访问某东,继续通过开发者工具观察这几个指标项:

我们可以看到,大部分的 size 由原来的大小变为了 disk cache(磁盘缓存)memory cache (内存缓存),而且 Time 列所对应的资源加载速度非常之快,加载总耗时由原来的1.05s变成了549ms,传输到本地的数据减少到了285kb,加载速度显著提升。这就是强缓存的作用。

流程如下所示:

由图中所示,我们看到浏览器没有和服务器进行数据交互,而是在发起请求时浏览器缓存告诉浏览器它存有该资源的缓存数据,所以浏览器就直接加载了缓存中的数据资源。

Expires 与 max-age

上一小节我们提到 Expires 设置的缓存过期时间是一个绝对时间,会受到客户端时间的影响,导致不精准。我们怎么理解这句话呢?我们看一下以下示例:

这是某金加载的一个图片资源,其首部 expires 字段值表示浏览器可以将该资源缓存至 2024 年 8 月 23 日的上述时间点,那么在我们把图中 max-age 首部当做不存在的情况下(因为 max-age 会覆盖 expires 值),把电脑客户端时间修改为 2022 年 8 月 28 日,此时再次访问网页你会发现浏览器重新向服务器获取了该资源,原来的缓存失效了。这就是 Expires “不精准”的原因。

2.4 协商缓存

协商缓存可以看作是强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

图中,HTTP请求先经历了强缓存的失效过程:浏览器发起 HTTP 请求后浏览器缓存发现该请求的资源失效,便将其缓存标识返回给浏览器,随后浏览器携带该缓存标识向服务器发起 HTTP 请求,之后服务器根据该标识判断这个资源其实没有更新过,最终返回304给浏览器,浏览器收到无更新的响应后便转向浏览器缓存获取数据。

缓存标识 Last-Modified 与 Etag

除了强缓存失效外,浏览器判断是否要走协商缓存还得借助上述提到的缓存标识:Last-Modified Etag,这两个首部字段我们在前面章节已经有所介绍,它们是服务器响应请求时返回的报头首部,如下图所示:

Etag 的优先级要高于 Last-Modified,当两者同时出现时,只有 Etag 会生效。只要有这两个缓存标识之一,在强缓存失效后浏览器便会携带它们向服务器发起请求,携带方式如下图请求头所示:

其中 if-Modified-since 对应 Last-Modified 的值,if-None-Match 对应 Etag 的值。服务器根据优先级高的缓存标识的值进行判断。如果 Etag 对应的 if-None-Match 不存在,那么服务器会将 last-modified 对应的 if-modified-since 的时间值与服务器该资源的最后修改时间进行对比,最后判断是否走协商缓存。

那么 Last-Modified 有会什么弊端?服务器进行对比时一定精准吗?

Last-Modified 是一个时间,最小单位为秒,试想一下,如果资源的修改时间非常快,快到毫秒级别,那么服务器会误认为该资源仍然是没有修改的,这便导致了资源无法在浏览器及时更新的现象。

另外还有一种情况,比如服务器资源确实被编辑了,但是其实资源的实质内容并没有被修改,那么服务器还是会返回最新的 Last-Modified 时间值,但是我们并不希望浏览器认为这个资源被修改而重新加载。

为了避免以上现象的发生,在特殊的场景下,我们便需要使用 Etag

node 中的 Etag 包为例,生成Etag的方式有两种,第一种方式是使用文件大小和修改时间生成。

另一种方式是通过使用文件内容的 hash 值和内容长度:通过对内容的 hash 转化和截取,最终返回内容长度与其 hash 值组合成的字符串。

通过以上方法生成的 Etag 称为强 Etag 值,即使发生细微的变化都会改变它的值。那么与其对立的便是弱 Etag 值,在 Etag 包源码中我们可以发现通过传递第二个参数 weak 值为 true 时便可启用弱校验。

注意:弱 ETag 值只适用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 Etag 值。这时会在字段值最开始处附加 W/。

启发式缓存

思考一下?如果响应报头中没有 max-age(s-maxage)expires 这两个关键的字段值时,浏览器还会进行强缓存吗?

如果是下面的报头:

代码语言:javascript复制
date: Thu, 08 Sep 2023 13:28:56 GMT
cache-control: public
age: 10467792
last-modified: Mon, 26 Apr 2023 09:56:06 GMT

虽然有与协商缓存相关的 Last-Modified 首部,但并不会走协商缓存,反而浏览器会触发启发式缓存。启发式缓存对于缓存有效期计算公式如下所示:

代码语言:javascript复制
缓存有效期 = max(0,(date - last-modified)) * 10%

资源数据会根据响应报头中 date 与 Last-Modified 值之差与 0 取最大值后取其值的百分之十作为缓存时间。

03 缓存方案

当我们访问首页时,浏览器率先加载的便是 HTML 文件,后续继续加载一些首页渲染需要以及公共的资源文件,当我们跳转页面时会异步加载下一个页面所需的资源,实现页面的组装及逻辑处理。我们以某宝加载资源的为例:

细心的同学可能会发现,刷新页面或再次访问时大部分资源都命中了强缓存,但是先加载的 HTML 资源走了协商缓存,这是为什么呢?

其实很容易理解,因为像 JS、CSS 等资源经过打包工具打包后可以自动生成带有 hash 的文件名,每次部署到服务器上后发生变化的资源 hash 名会更新,浏览器会当作一个新的资源去向服务器请求,没有更新的资源便会优先读取浏览器缓存。而 HTML 不同,其文件名不会改变,我们期望浏览器每次加载时都应该向服务器询问是否更新,否则会出现新版本发布后浏览器读取缓存 HTML 文件导致页面空白报错(旧资源被删除)或应用没有更新(读取了旧资源)的问题。

根据 HTTP 缓存的规则最终我们便可以总结出如下缓存方案:

  • 频繁变动的资源,比如 HTML, 采用协商缓存
  • CSS、JS、图片资源等采用强缓存,使用 hash 命名

比如 JQuery 时代我们的资源文件一般通过在 HTML 中直接引入的方式来进行加载,同时会加上一段时间戳或者版本号代码:

代码语言:javascript复制
<script src="./js/demo.js?version=2.0"></script>

因为浏览器会缓存之前的 JS、CSS 版本,通过上述添加类似于 hash 值的方式能够让浏览器加载最新的版本。

那么我们怎么让 HTML 文件走协商缓存呢?首先要让浏览器强缓存失效,可以设置如下服务器响应报头:

代码语言:javascript复制
Cache-Control: max-age=0
Last-Modified: Sat, 04 Sep 2023 12:49:33 GMT

通过设置资源 0 秒就失效的情况下且又存在协商缓存触发条件的 Last-Modified 标识,这样的话每次访问加载的 HTML 资源就可以确保是最新的,解决了 HTML被浏览器强缓存导致资源没有更新的问题。

04 浏览器缓存

浏览器缓存是指浏览器在访问网页时将一些数据存储在本地计算机上的过程。这样做的目的是提高网页加载速度和用户体验,减少对远程服务器的请求,以及降低网络流量消耗,浏览器缓存可以存储多种类型的数据。

4.1 Memory Cache 与 Disk Cache

Memory Cache(内存缓存)和 Disk Cache(磁盘缓存)都是计算机系统中用于存储数据以提高性能的缓存类型。它们的主要区别在于数据存储的位置和访问速度。

Memory Cache

Memory Cache 将数据存储在计算机的内存中,内存的读取速度非常快,几乎接近零延迟,这使得从内存中检索数据比从硬盘中检索数据要快得多。因此,内存缓存可以在非常短的时间内提供存储在其中的数据。

但是 Memory Cache 受计算机内存影响,容量有限,不能存储大容量数据,且生命周期短,当关闭页面,就会释放资源,属于临时存储。

如果要存储大容量数据,是无法进行存储的。

Disk Cache

Disk Cache 将数据存储在硬盘上,是一种永久性存储介质,相比内存,磁盘容量大很多,但相对于内存,硬盘的读取速度较慢。

数据在 Disk Cache 中通常更持久,因为它们不会因进程或应用程序的关闭而被删除。但是,这些数据可能会定期被清理释放存储资源。

Memory Cache 与 Disk Cache 属于互补关系,共同组成了浏览器本地缓存的左膀右臂。

4.2 缓存机制

浏览器的缓存系统通常分为三个级别,分别是内存缓存、磁盘缓存和网络缓存。简称浏览器三级缓存。

三级缓存获取顺序依次是内存缓存、磁盘缓存、和网络缓存。浏览器首先检查内存缓存,如果资源存在并未过期,立即加载;否则,它检查磁盘缓存,如果资源在磁盘中存在且有效,也会加载。如果资源既不在内存缓存也不在磁盘缓存中,浏览器将从网络请求资源,然后将其存入缓存中供以后使用,这一顺序有助于提高加载性能并减少对远程服务器的请求。

缓存存储优先级

浏览器在决定是否将资源存储在内存缓存还是磁盘缓存中时,通常依赖于资源的类型、大小和访问频率等因素。浏览器判断使用哪种缓存的一些常见依据有:

  1. 浏览器通常将页面的核心HTML、CSS、JavaScript文件和小型图像等频繁使用的资源存储在内存缓存中,因为内存访问速度非常快,适合快速访问这些资源。较大的资源文件、音视频文件和其他不经常变化的资源可能更适合存储在磁盘缓存中。
  2. 浏览器可能会根据资源的访问频率来做决策。频繁访问的资源更有可能存储在内存中,以提高响应速度,而不经常访问的资源可能存储在磁盘上,以腾出内存空间供更常用的资源使用。
  3. 用户设备的性能也可能影响缓存位置的选择。性能较低的设备可能会更多地依赖于磁盘缓存,而性能较高的设备则可能更多地使用内存缓存。
  4. 用户在页面上的行为也可能影响缓存的选择。如果用户在页面上进行了交互,可能会导致某些资源被存储在内存缓存中,以便更快地响应交互。

总之,浏览器的内存缓存和磁盘缓存之间的决策通常是动态的,并受多个因素的影响。浏览器会根据资源的类型、访问频率等因素来决定如何最优地管理缓存,以提供最佳的性能和用户体验。这种自动的缓存管理有助于加速页面加载速度和减少对网络资源的依赖。

Preload 与 Prefetch

preload 也被称为预加载,其用于 link 标签中,可以指明哪些资源是在页面加载完成后即刻需要的,浏览器会在主渲染机制介入前预先加载这些资源,并不阻塞页面的初步渲染。例如:

代码语言:javascript复制
<link rel="preload" href="http://res.wx.qq.com/open/js/jweixin-1.6.0.js" as="script" />

而当使用 preload 预加载资源后,笔者发现该资源一直会从磁盘缓存中读取,JS、CSS 及图片资源都有同样的表现,这主要还是和资源的渲染时机有关,在渲染机制还没有介入前的资源加载不会被内存缓存。

相反 prefetch 则表示预提取,告诉浏览器加载下一页面可能会用到的资源,浏览器会利用空闲状态进行下载并将资源存储到缓存中。

代码语言:javascript复制
<link rel="prefetch" href="http://res.wx.qq.com/open/js/jweixin-1.6.0.js" as="script"/>

使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。

通过使用 preload prefetch ,我们可以优化浏览器资源加载的顺序和时机,从而获取更好的用户体验。

4.3 Service Worker

Service Worker 是一种在 Web 浏览器中运行的脚本,它可以作为浏览器和网络之间的代理,用于实现诸如离线访问、推送通知、后台数据同步等功能。Service Worker 独立于网页页面,工作方式是事件驱动的,它可以监听并响应来自浏览器的事件,一旦Service Worker被注册并激活,它将在后台运行,即使用户关闭了网站也可以继续工作。

Service Worker 在其生命周期中会经历三个步骤:注册、安装、激活。

注意:出于安全考虑,Service worker 只能在 https 及 localhost 下被使用。

PWA:渐进式Web应用(Progressive Web App,PWA)是一种结合了Web和原生应用功能的现代Web应用程序开发方法。PWA旨在提供与原生应用类似的用户体验,包括快速加载、离线访问、推送通知等功能,同时仍然可以通过浏览器访问,无需安装或下载。

Service Worker 是创建PWA的关键组成部分之一。PWA通常以Service Worker为核心,因为Service Worker可以用于缓存资源、处理离线请求,以及向用户发送推送通知等。

我们这次演示一下 Service Worker 推送事件和监听事件的 chrome 调试查看流程:

网站带有这个标识的都是PWA应用:

我们以PWA应用为例,如下所示,我们先点击push messaging 然后点击捕获按钮,开始监听Service Worker事件推送:

然后推送一个事件,点击push按钮:

我们再回到push messaging 看下,我们发现推送事件已经成功了

大家也可以去尝试一下,感受它的强大。关于其它 Service Worker 更多细节,在此就不做过多介绍,感兴趣的读者可以后续详细了解下。

05 总结

当我们探讨前端缓存时,我们不仅仅是在谈论如何加速网页加载,更是在谈论提供更出色的用户体验。缓存不仅可以减少网络请求,提高性能,还可以实现离线访问、推送通知等现代Web应用所需的关键功能。通过使用合适的缓存策略,我们可以有效地管理资源,确保用户在不同设备和网络条件下都能够获得无缝的体验。

然而,缓存并非没有挑战。需要仔细考虑缓存清除、缓存策略、数据更新等方面的问题,以确保数据的一致性和安全性。

最重要的是,前端缓存是Web开发中的一项关键技术,需要不断学习和适应不断发展的Web生态系统。通过不断深入研究和实践,我们可以更好地利用前端缓存来提升我们的项目性能,提高用户体验。

希望本文可以给你带来一些帮助!

【选题思路】

随着现代Web应用不断演进,前端性能优化已成为开发者们的“头等大事”。然而,随着页面变得更加复杂和数据密集,传统的网络请求方式已无法满足用户对速度和响应性的需求。前端缓存策略作为提高Web应用性能的重要手段,备受开发人员和企业关注。前端缓存不仅可以减少服务器负载,还可以节省用户的时间和带宽。但是,实施有效的前端缓存策略并不是一项轻松的任务,它涉及到多种技术和考虑因素。在这篇文章中,我们将深入探讨前端缓存的重要性以及实际实践中的挑战。

笔者也曾对缓存有诸多不了解之处,在学习了前端缓存技术与方案解析后,有感而发,归纳总结,通过这次机会巩固自身知识并分享给社区开发人员,希望能帮助到对此有疑惑的人,我们将首先回顾前端缓存的基本概念,包括缓存的类型以及它们在Web应用性能中的作用。然后,我们将深入研究前端缓存策略的关键组成部分,包括缓存机制、缓存失效策略和数据更新机制。

【创作提纲】

1.前端缓存的基本概念

2.前端缓存之HTTP缓存

3.前端缓存的常见策略

4.前端缓存之浏览器缓存

5.总结

0 人点赞