# 前端浏览器缓存
浏览器缓存作为性能优化的重要一环,对于前端而言,重要性不言而喻。我想几乎每个开发者都碰到过缓存的问题吧,甚至有很多情况下我们会说这个问题已经修复了,你清理下缓存就好了。这篇文章我们就细细的来挖掘下缓存的种种轶事。
# 1. 缓存介绍
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。
很多开发者习惯把cookie、webStorage以及IndexedDB存储的数据也称之为缓存,理由是都是保存在客户端的数据,没有什么区别。其实这是不严谨的,cookie的存在更多的是为了让服务端区别用户,webStorage和IndexedDB则更多用在保存具体的数据和在客户端存储大量结构化数据(文件/blobs)上面。
实际上所谓的缓存只有一种——Web缓存。它是指一个Web资源(如html页面,图片,js,数据等)存在于Web服务器和客户端(浏览器)之间的副本。HTTP协议里定义了很多关于缓存的请求和响应字段,这也是接下来我们重点要了解熟悉的对象,研究下究竟是哪些字段怎么影响缓存的。
# 2. 缓存规则
http缓存规则由响应首部字段进行控制,其中的关键字段有Expires、Cache-Control、Last-Modified、Etag四个字段。Expires和Cache-Control用来确定确定缓存的存储时间,Last-Modified 和Etag则用来确定缓存是否要被更新。
| 关键字段 | 描述 |
|---|---|
| Expires | HTTP1.0中用来控制缓存时间的参数,绝对时间, 即在此时间之后,响应过期。 |
| Cache-Control | HTTP1.1中用来控制缓存时间的参数,相对时间。 |
| Last-Modified | 源头服务器认定的资源做出修改的日期及时间。 |
| Etag | HTTP响应头是资源的特定版本的标识符。 |
# 3. 缓存流程
目前主流的浏览器缓存分为两类,强缓存和协商缓存,它们的匹配流程如下:
- 浏览器发送请求前,根据请求头的expires和cache-control判断是否命中强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。
- 没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。
- 如果前两步都没有命中,则直接从服务端获取资源。

# 4. 强缓存
# 4.1 强缓存配置
强缓存可以通过服务端设置expires和cache-control来控制。
server {
listen 80;
server_name www.liam.huoyuhao.com;
root /www/liamTest/;
index index.html;
charset utf-8;
location ~ .*\.(gif|jpg|png)(.*) {
expires 10s;
add_header wall "hello liam!!!";
}
}
2
3
4
5
6
7
8
9
10
11
# 4.2 强缓存响应头

(1)expires:从图可以看出,expires的值是一个绝对时间,是http1.0的功能。如果浏览器的时间没有超过这个expires的时间,代表缓存还有效,命中强缓存,直接从缓存读取资源。不过由于存在浏览器和服务端时间可能出现较大误差,所以在之后http1.1提出了cache-control。
(2)cache-control:从图可以看出,cache-control的值是类似于max-age=10这样的,是一个相对时间,10是秒数。当浏览器第一次请求资源的时候,会把response header的内容缓存下来。之后的请求会先从缓存检查该response header,通过第一次请求的date和cache-control计算出缓存有效时间。如果浏览器的时间没有超过这个缓存有效的时间,代表缓存还有效,命中强缓存,直接从缓存读取资源。
两者可以同时设置,但是优先级cache-control > expires。
# 4.3强缓存作用
强缓存作为性能优化中缓存方面最有效的手段,能够极大的提升性能。由于强缓存不会向服务端发送请求,对服务端的压力也是大大减小。对于不太经常变更的资源,可以设置一个超长时间的缓存时间,比如一年。浏览器在首次加载后,都会从缓存中读取。 但是由于不会向服务端发送请求,那么如果资源有更改的时候,怎么让浏览器知道呢?现在常用的解决方法是加一个?v=xxx的后缀,在更新静态资源版本的时候,更新这个v的值,这样相当于向服务端发起一个新的请求,从而达到更新静态资源的目的。(浏览器缓存资源的匹配规则的key是与资源请求URL的全链接相关的,所以更改后缀,浏览器找不到缓存资源)
# 5. 协商缓存
# 5.1 协商缓存原理
在强缓存没有命中的时候,就是协商缓存发挥的地盘了。协商缓存会根据[last-modified/if-modified-since]或者[etag/if-none-match]来进行判断缓存是否过期。

(1)last-modified/if-modified-since: 浏览器首先发送一个请求,让服务端在response header中返回请求的资源上次更新时间,就是last-modified,浏览器会缓存下这个时间。然后浏览器再下次请求中,request header中带上if-modified-since:[保存的last-modified的值]。根据浏览器发送的修改时间和服务端的修改时间进行比对,一致的话代表资源没有改变,服务端返回正文为空的响应,让浏览器中缓存中读取资源,这就大大减小了请求的消耗。由于last-modified依赖的是保存的绝对时间,还是会出现误差的情况:一是保存的时间是以秒为单位的,1秒内多次修改是无法捕捉到的;二是各机器读取到的时间不一致,就有出现误差的可能性。为了改善这个问题,提出了使用etag。
(2)etag/if-none-match:etag是http协议提供的若干机制中的一种Web缓存验证机制,并且允许客户端进行缓存协商。生成etag常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。 和last-modified一样,浏览器会先发送一个请求得到etag的值,然后再下一次请求在request header中带上if-none-match:[保存的etag的值]。通过发送的etag的值和服务端重新生成的etag的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。
etag能够解决last-modified的一些缺点,但是etag每次服务端生成都需要进行读写操作,而last-modified只需要读取操作,从这方面来看,etag的消耗是更大的。
# 5.2 协商缓存作用
协商缓存是无法减少请求数的开销的,但是可以减少返回的正文大小。一般来说,对于勤改动的html文件,使用协商缓存是一种不错的选择。
# 6. 刷新缓存
刷新强缓存可以使用?v=xxx的后缀。当然,人工更改版本号的成本比较高,而且难以维护,现在主流的是通过webpack等打包工具生成[name].[hash].js之类的文件名,也能刷新强缓存。
刷新协商缓存比较简单,修改文件内容即可。
对于浏览器而言,在Chrome中,你可以使用审查元素,高版本也叫检查,将Network中的Disable cache打勾,使用cmd+r刷新页面即可。当然你也可以使用强制刷新,直接在页面使用cmd+shift+r进行刷新。
# 7. 缓存最佳实践
# 7.1 静态资源缓存策略
对于不同类型的资源,应采用不同的缓存策略:
# Nginx缓存配置示例
server {
# HTML文件不缓存或短期缓存
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# CSS/JS文件长期缓存,通过文件名hash控制更新
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 图片文件中等期限缓存
location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public";
}
# 字体文件长期缓存
location ~* \.(woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 7.2 文件名哈希策略
使用构建工具为静态资源生成唯一哈希值,实现缓存 busting:
// Webpack配置示例
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
]
};
2
3
4
5
6
7
8
9
10
11
12
# 7.3 Service Worker缓存
使用Service Worker实现更精细的缓存控制:
// service-worker.js
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 如果缓存中有响应,直接返回
if (response) {
return response;
}
return fetch(event.request);
})
);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 8. 其他
# 8.1 from disk cache和from memory cache区别
# 1)form memory cache
不请求网络资源,资源在内存当中,一般脚本、字体、图片会存在内存当中。浏览器关闭后,数据将不存在(资源被释放掉了),再次打开相同的页面时,不会出现from memory cache。
# 2)form disk cache
不请求网络资源,在磁盘当中,一般非脚本会存在内存当中,如css等。闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉下次打开仍然会是from disk cache。
# 3)几种状态的执行顺序
现加载一种资源(例如:图片):
访问-> 200 -> 退出浏览器
再进来-> 200(from disk cache) -> 刷新 -> 200(from memory cache)
# 4)不同浏览器策略不同
以上的数据及统计都是在chrome浏览器下进行的
在Firefox下并没有from memory cache以及from disk cache的状态展现
# 8.2 Cache-Control的值
1)可缓存性
| 属性 | 含义 |
|---|---|
| public | 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存 |
| private | 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容 |
| no-cache | 在释放缓存副本之前,强制高速缓存将请求提交给原始服务器进行验证 |
| only-if-cached | 表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝 |
2)到期
| 属性 | 含义 |
|---|---|
| max-age = number | 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。 |
| s-maxage = number | 覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。 |
| max-stale[ = number] | 表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。 |
| min-fresh = number | 表示客户端希望在指定的时间内获取最新的响应。 |
| stale-while-revalidate = number | 表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。秒值指示客户愿意接受陈旧响应的时间长度。 |
| stale-if-error = number | 表示如果新的检查失败,则客户愿意接受陈旧的响应。秒数值表示客户在初始到期后愿意接受陈旧响应的时间。 |
3)其他
| 属性 | 含义 |
|---|---|
| no-store | 缓存不应存储有关客户端请求或服务器响应的任何内容。 |
| no-transform | 不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-Type等HTTP头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。 no-transform指令不允许这样做。 |
# 9. 缓存优化技巧
# 9.1 合理设置缓存时间
# 长期缓存的静态资源
Cache-Control: public, max-age=31536000
# 短期缓存的动态资源
Cache-Control: public, max-age=300
# 不缓存的敏感资源
Cache-Control: no-cache, no-store, must-revalidate
2
3
4
5
6
7
8
# 9.2 使用CDN加速
# 配置CDN缓存策略
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
2
3
4
5
6
# 9.3 缓存预加载
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//example.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="/critical.css" as="style">
<!-- 预连接 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
2
3
4
5
6
7
8
# 10. 常见问题及解决方案
# 10.1 缓存更新不及时
问题:用户访问网站时看到的是旧版本内容
解决方案:
- 使用文件名hash策略
- 设置合理的缓存时间
- 在关键资源上使用版本号
# 10.2 缓存穿透
问题:大量请求绕过缓存直接访问服务器
解决方案:
- 使用布隆过滤器过滤无效请求
- 设置合理的缓存空值策略
- 限流保护
# 10.3 缓存雪崩
问题:大量缓存在同一时间失效,导致服务器压力骤增
解决方案:
- 设置不同的过期时间
- 使用互斥锁或分布式锁
- 缓存预热机制
# 11. 【参考文章】
http缓存与cdn缓存配置指南 (opens new window)