为什么需要缓存?
在任何一个前端项目中,访问服务器获取数据都是很常见的事情,如果相同的数据被重复请求了不止一次,那么多余的请求必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,多余的请求还会隐形的增加用户的网络流量资费。因此考虑使用缓存技术对已经获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。
缓存的原理
缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免了重新向服务器发起资源请求
缓存的执行顺序
当刷新浏览器的时候
查找强缓存(memory Cache 和 disk Cache)中是否有所需资源,如果有,直接返回,若没有,执行第2步。
当缓存没有命中,则直接发送http请求,如果工程代码中存在sevice worker,则执行第3步。
service worker(简称sw) 拦截到http请求,进行自定义的处理(sw的代码逻辑)。如果sw并未进行任何缓存操作,则sw与服务器进行接口请求通讯。
如果该文件属于协商缓存,则服务器进行协商缓存的验证。协商缓存命中则返回304 状态码。未命中则返回文件内容。
浏览器主进程拿到请求返回的资源后进行加载和渲染等。
缓存的种类
memory cache 内存中的缓存,速度快,持续性短。
- 强缓存
- 协商缓存
- service cache 自由控制缓存哪儿写文件
- push cache 只在会话session 中存在,会话结束就被释放,并且缓存时间也很短暂。
- preload cache 页面加载后立即预加载的资源
- Web Storage、IndexedDB 也可以让我们实现某些功能是,通过代码做一些存储处理。
缓存的位置说明
memory cache 内存中的缓存,速度快,持续性短。
disk cache 缓存在磁盘中,读取速度较慢
memory cache > disk cache
在 Windows 上,Chrome 的缓存文件通常存储在以下目录中:C:\Users\{username}\AppData\Local\Google\Chrome\User Data\Default\Cache
在 macOS 上,Chrome 的缓存文件通常存储在以下目录中:~/Library/Caches/Google/Chrome/Default/Cache
在 Linux 上,Chrome 的缓存文件通常存储在以下目录中:~/.cache/google-chrome/Default/Cache
下面说明
当我们第一次刷新页面时,浏览器从服务器下载资源,然后判断文件是否需要缓存【强缓存,协商缓存】。
如果需要缓存则此时的文件缓存在 memory cache 中。当前页面刷新即可看到,如下面【图1】 所示
此时关闭浏览器窗口,内存清空,会将文件缓存到 dist cache中,如在浏览器新的tab页打开,结果如【图2】所示
再次从当前页面刷新窗口时,依旧是如【图1】所示,此时已经将需要缓存的资源读取到内存中。
图1如下
图2 如下
浏览器的缓存方式,强缓存、协商缓存、sevice worker。
通过上面的介绍,我们已经知道了缓存的执行顺序,接下来我们就说一下各个缓存
1. 强缓存
强缓存依据两个字段来设置
(1)Expires 表示缓存到期时间,以格林尼治标准时间当作值。如:Expires: Mon, 03 Jul 2023 08:16:05 GMT
(2)Cache-Control 的值是一个以秒为单位的时间长度,Cache-Control: max-age=50表示该资源在被请求到后的50s内有效。其他参数如下:
-
no-cache:表示需要使用缓存,但是浏览器不进行验证,需要服务器验证,如果缓存有效,则服务器会返回304 Not Modified响应,否则会返回新的资源内容。 -
no-store:表示不使用缓存,每次请求都需要向服务器请求新的资源。这个选项会禁止浏览器和代理服务器缓存响应,以确保每次请求都会获取最新的资源。 -
max-age:表示响应可以被缓存的最长时间,以秒为单位。例如,max-age=3600表示响应可以被缓存1小时。在这段时间内,浏览器和代理服务器可以使用缓存,而不需要向服务器发送请求。 -
s-maxage:与max-age类似,但仅适用于代理服务器。它指定了代理服务器可以缓存响应的最长时间,而不考虑客户端缓存的情况。 -
public:表示响应可以被任何缓存(包括客户端和代理服务器)缓存。 -
private:表示响应只能被客户端缓存,而不能被代理服务器缓存。 -
no-transform:表示不允许代理服务器修改响应的内容,例如压缩或修改图片格式等。 -
must-revalidate:表示如果缓存过期,则必须向服务器发送请求进行验证。如果验证失败,则需要重新下载资源。 -
proxy-revalidate:与must-revalidate类似,但仅适用于代理服务器。这些Cache-Control值可以组合使用,以达到更细粒度的缓存控制。例如,
Cache-Control:max-age=3600, public表示响应可以被任何缓存缓存,并且可以在1小时内使用缓存。如果在1小时后再次请求资源,则需要向服务器发送请求进行验证。
注意:如果Cache-Control的max-age和expires同时存在,则以max-age为准。
2.协商缓存
2.1 基于 laset-modified 的协商缓存。
客户端第一次请求目标资源的时,服务器返回的响应标头包含last-modified和该资源的最后一次修改的时间戳,以及cache-control:no-cache。如下图所示
当客户端再次请求该资源的时候,会携带一个if-modified-since字段,如果这个字段对应的时间与目标资源的时间戳进行对比,如果没有变化则返回一个304状态码。
需要注意的是:协商缓存判断缓存有效的响应状态码是304,但是如果是强制缓存判断有效的话,响应状态码是200。如下图所示:
laset-modified缺点:
last-modified是根据请求资源的最后修改的时间戳来进行判断的,如只是添加空格或者换行等,内容并未变化时,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求,会造成带宽浪费。
文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
// 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>
what does that mean?
</h1>
<img src="./img/infinity.jpg" width="500">
<script src="./js/noCache.js"></script>
<script src="./js/cacheJs/first.js"></script>
</body>
</html>
// 服务端代码设置 nodejs
var fs = require('fs');
var path = require("path")
exports.setCache = function(name, response, request) {
if (name.includes('first.js')) {
const fileName = '/font/js/cacheJs/first.js';
const parentDir = path.join(__dirname, '../'); // 获取上一级文件夹的路径
const filePath = path.join(parentDir, fileName); // 获取文件的完整路径
const { mtime } = fs.statSync(filePath);
const ifModifiedSince = request.headers['if-modified-since'];
// console.log('进行协商缓存', mtime.toUTCString(), ifModifiedSince)
if (ifModifiedSince === mtime.toUTCString()) {
response.statusCode = 304;
response.end();
return true
}
response.setHeader('last-modified', mtime.toUTCString());
response.setHeader('Cache-Control','no-cache');
}
}
2.2 基于 Etag 协商缓存的流程
客户端第一次请求资源时,服务器会将该资源返回给客户端,并在响应头中添加Etag字段,该字段的值是该资源的唯一标识符,通常是该资源的哈希值。
客户端再次请求该资源时,在请求头中添加If-None-Match字段,并将上次请求中服务器返回的Etag值作为该字段的值,表示客户端希望服务器检查该资源是否有更新。
服务器收到请求后,将请求头中的If-None-Match字段中的值与服务器端的Etag值进行比较。如果两者相等,则表示该资源没有更新,可以使用缓存;如果两者不相等,则表示该资源已经更新,需要重新获取该资源。
如果服务器判断该资源没有更新,则返回304 Not Modified响应,并在响应头中添加与第一次请求相同的Etag字段,表示该资源的Etag值没有发生改变。
如果服务器判断该资源已经更新,则返回200 OK响应,并重新发送该资源的内容以及新的Etag值。
Etag缺点:
`Etag`协商缓存的主要思想是通过比较资源的唯一标识符(Etag值)来判断资源是否有更新,从而实现缓存控制。
相对于`Last-Modified`协商缓存,`Etag`协商缓存可以更精确地判断资源是否有更新,但由于需要服务器计算`Etag`值,并且需要将该值存储在服务器端,因此相对来说更加耗费资源。
`Etag`字段值的生成分为强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同,弱验证则根据资源的部分属性来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为准确不够而降低协商缓存有效性的成功率,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。
不像强制缓存中cache-control可以完全替代expires的功能,在协商缓存中,Etag并非last-modified的替代方案而是一种补充方案。
关于缓存的更详细的介绍,参考 该篇文章。
3.service worker
`service worker` 的功能类似于代理服务器,允许你去修改请求和响应,将其替换成来自其自身缓存的项目。
由于Service Worker运行在浏览器的后台线程中,因此对于同一个域名,同一时刻只能有一个Service Worker在运行,即使它们的Scope不同。
service worker 中的scope 是以野咩那
3.1 我们先创建一个service worker 吧
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
// sw.js是service worker代码执行的文件。
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/index/", // 指定的拦截的页面范围。
});
if (registration.installing) {
console.log("正在安装 Service worker");
} else if (registration.waiting) {
console.log("已安装 Service worker installed");
} else if (registration.active) {
console.log("激活 Service worker");
}
} catch (error) {
console.error(`注册失败:${error}`);
}
}
};
说明:
scope的路径指定的是页面的反问,如上所示 `/index/`表示拦截 localhos:9000/index/index.html内的所有的资源请求。
scope 并不能设置只拦截某个文件夹内的js或者img,sw 会拦截当前页面所有的请求。
3.2 接下来我们就可以进行一些缓存的操作
监听service worker 的安装事件,进行某些初始化文件的缓存
const addResourcesToCache = async (resources) => {
const cache = await caches.open("v5");
await cache.addAll(resources);
};
self.addEventListener("install", (event) => {
console.log('Service Worker installed...');
// 会确保 Service Worker 不会在 `addResourcesToCache()` 里面的代码执行完毕之前安装完成。
event.waitUntil(
addResourcesToCache([
"/js/cacheJs/first.js",
])
);
});
// 安装完成后sw 被激活
self.addEventListener('activate', function(event) {
console.log('Service Worker activating...');
});
监听fetch事件,进行请求拦截,按需进行缓存等操作
// 添加缓存
const putInCache = async (request, response) => {
const cache = await caches.open("v5");
await cache.put(request, response);
};
const cacheFirst = async (request) => {
// 用于在 Service Worker 的缓存中查找与请求匹配的响应,如果找到则返回响应,否则返回 `undefined`。
const responseFromCache = await caches.match(request);
// console.log(responseFromCache, '****', caches, request)
if (responseFromCache) {
return responseFromCache;
}
// 未找到缓存中的匹配,进行接口请求
const responseFromNetwork = await fetch(request);
putInCache(request, responseFromNetwork.clone());
return responseFromNetwork;
};
// fetch 事件只有在 安装和激活后,方可被执行
self.addEventListener("fetch", (event) => {
console.log('Fetch event received...');
event.respondWith(cacheFirst(event.request));
});
说明 :
在 Service Worker 第一次安装完成后,它会被激活并开始拦截网络请求。
如果 [Service Worker]() 文件发生更改,浏览器会重新下载并安装新的 Service Worker 文件,并启动新的 Service Worker 实例。
新的 Service Worker 实例会等待旧的 Service Worker 实例不再控制任何客户端,然后激活并开始拦截网络请求。
fetch 事件只有在安装和激活事件后,方可被执行。
注意事项
- 我们本地开发和测试时,切记每次要将之前的service worker 在控制台注销掉,避免和自己预期不一致
2. 我们缓存的内容都在cache storage 中查看
此段内容只是对service worker 的简单介绍,详情请查看 MDN
preload
简单的描述下预加载吧,也是提高页面性能的,算是一个提前的缓存哇。
preload 是一个 HTML 标签属性,用于指定需要在页面加载后立即预加载的资源。预加载资源可以是 JavaScript 文件、样式表、图片等,它们通常是页面中重要的资源,可以提高页面的性能和用户体验。
使用 preload 属性可以让浏览器在页面加载后立即开始预加载指定的资源,而无需等待 JavaScript 或 CSS 文件的解析和执行。这可以减少页面加载时间并提高性能。
<!DOCTYPE html>
<html>
<head>
<title>Preload Example</title>
<link rel="preload" href="styles.css" as="style">
<link rel="preload" href="main.js" as="script">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Hello, World!</h1>
<script src="main.js"></script>
</body>
</html>
我们使用 link 标签指定需要预加载的 CSS 和 JavaScript 文件,并分别将它们的 as 属性设置为 style 和 script。这告诉浏览器需要预加载的资源类型,并在页面加载后立即开始预加载指定的资源。