实际上,HTTP 传输的每一个环节基本上都会有缓存,非常复杂。
HTTP Cache 是我们开发中接触最多的缓存,它分为强缓存和协商缓存。
- 强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是
200
。 - 协商缓存:会去服务器比对,若没改变才直接读取本地缓存,返回的状态码是
304
。
缓存优先级
Pragma
> Cache-Control
> Expires
> ETag
> Last-Modified
流程图
强缓存
Expires
当我们请求一个资源,服务器返回时,可以在 Response Headers
中增加 expires
字段表示资源的过期时间。是一个绝对时间。
::::tabs
::: tab 效果示例
响应 | 60s 内刷新页面 |
---|---|
在 60 秒内修改返回值 response.end("console.log('script loaded xxxx')")
,再刷新页面重新请求可以看到浏览器输出的仍然是 script loaded
说明浏览器并没有请求新的文件而是读取本地缓存。
:::
::: tab 代码示例
const http = require('http');
http
.createServer(function (request, response) {
console.log(request.url);
if (request.url === '/') {
// 指定 html 不然不识别为 html;指定编码 utf-8 不然中文乱码
response.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
response.end(`<h2>强缓存</h2><script src="/script.js"></script>`);
}
if (request.url === '/script.js') {
const now = new Date();
now.setSeconds(now.getSeconds() + 60); // 获取当前时间的 60 秒后
response.writeHead(200, {
'Content-Type': 'text/javascript',
Expires: now.toGMTString(),
});
response.end("console.log('script loaded')");
}
})
.listen(3300);
console.log('http://127.0.0.1:3300');
:::
::::
因为 Expires 表示资源的过期绝对时间,使用 Expires 需要保持服务器和客户端的时间一致,才可以保证缓存起到正确的作用。不好,所以基本被摒弃了。
Cache-Control
正由于上面说的可能存在的问题,HTTP1.1 新增了 Cache-Control
字段来解决该问题,所以当 Cache-Control 和 expires 都存在时,Cache-Control 优先级更高。
'Cache-Control': 'max-age=2000' // 过期时间 2000s
public
: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存private
: 用户的本地浏览器才可以缓存no-store
: 不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面; 浏览器和中间代理服务器都不能缓存资源。no-cache
: 跳过当前的强缓存,发送 HTTP 请求,即直接进入协商缓存阶段。must-revalidate
: 如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。max-age=[seconds]
: 单位秒,值为多少秒就缓存多久s-maxage=[seconds]
: 覆盖 max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
协商缓存
强缓存失效之后,浏览器在请求头中携带相应的缓存 tag
来向服务器发请求,由服务器根据这个 tag
,来决定是否使用缓存,这就是协商缓存。
Last-Modified
::::tabs
::: tab Last-Modified/If-Modified-Since
last-modified
记录资源最后修改的时间。启用后,请求资源之后的响应头会增加一个 last-modified
字段,当再次请求该资源时,请求头中会带有 if-modified-since
字段
服务端会对比该字段和资源的最后修改时间,若一致则证明没有被修改,告知浏览器可直接使用缓存并返回 304
;若不一致则直接返回修改后的资源,并修改 last-modified
为新的值。
:::
::: tab 代码示例
const http = require('http');
const now = new Date();
setTimeout(() => {
now.setSeconds(now.getSeconds() + 1);
console.log('Last-Modified change:', now.toGMTString());
}, 60000);
http
.createServer(function (request, response) {
if (request.url === '/') {
response.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
response.end(`<h2>协商缓存</h2><script src="/script.js"></script>`);
}
if (request.url === '/script.js') {
const isModifiedSince = request.headers['if-modified-since'];
const lastModified = now.toGMTString();
if (isModifiedSince === lastModified) {
// 如果资源未修改 则返回 304
response.writeHead(304, {
'Content-Type': 'text/javascript',
'Cache-Control': 'no-cache',
'Last-Modified': lastModified,
});
response.end();
} else {
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control': 'no-cache', // no-cache 进行协商缓存
'Last-Modified': lastModified, // 设置上次修改时间 配合 If-Modified-Since 或者 If-Unmodified-Since 使用
});
response.end("console.log('script loadedxx')");
}
}
})
.listen(3300);
console.log('http://127.0.0.1:3300');
:::
::: tab network 面板
刷新主页,60s 内刷新页面,就可以看到:
pic1 | pic2 |
---|---|
:::
::::
但 last-modified 有以下两个缺点:
- 只要编辑了,不管内容是否真的有改变,都会以这最后修改的时间作为判断依据,当成新资源返回,从而导致了没必要的请求响应,而这正是缓存本来的作用即避免没必要的请求。
- 时间的精确度只能到秒,如果在一秒内的修改是检测不到更新的,仍会告知浏览器使用旧的缓存。
Etag
ETag
是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。
浏览器接收到 ETag 的值,会在下次请求时,将这个值作为 If-None-Match
这个字段的内容,并放到请求头中,然后发给服务器。
服务器接收到 If-None-Match
后,会跟服务器上该资源的 ETag 进行比对:
- 如果两者不一样,说明要更新了。返回新的资源,跟常规的 HTTP 请求响应的流程一样。
- 否则返回 304,告诉浏览器直接用缓存。
两者对比
在精准度上,ETag 优于 Last-Modified。优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。而 Last-Modified 就不一样了,它在一些特殊的情况并不能准确感知资源变化,主要有两种情况:
- 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
- Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。
在性能上,Last-Modified 优于 ETag,也很简单理解,Last-Modified 仅仅只是记录一个时间点,而 Etag 需要根据文件的具体内容生成哈希值。
缓存位置
前面我们已经提到,当强缓存命中或者协商缓存中服务器返回 304 的时候,我们直接从缓存中获取资源。那这些资源究竟缓存在什么位置呢?
浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
Service Worker
Service Worker 借鉴了 Web Worker 的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问 DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存、消息推送和网络代理等功能。其中的离线缓存就是 Service Worker Cache
。
Service Worker 同时也是 PWA 的重要实现机制...
Memory Cache 和 Disk Cache
Memory Cache 指的是内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。
Disk Cache 就是存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。稍微有些计算机基础的应该很好理解,就不展开了。
好,现在问题来了,既然两者各有优劣,那浏览器如何决定将资源放进内存还是硬盘呢?主要策略如下:
- 比较大的 JS、CSS 文件会直接被丢进磁盘,反之丢进内存
- 内存使用率比较高的时候,文件优先进入磁盘
Push Cache
即推送缓存,这是浏览器缓存的最后一道防线。它是 HTTP/2 中的内容,虽然现在应用的并不广泛,但随着 HTTP/2 的推广,它的应用越来越广泛。关于 Push Cache,有非常多的内容可以挖掘,不过这已经不是本文的重点,大家可以参考这篇扩展文章。