# 前端浏览器缓存

浏览器缓存作为性能优化的重要一环,对于前端而言,重要性不言而喻。我想几乎每个开发者都碰到过缓存的问题吧,甚至有很多情况下我们会说这个问题已经修复了,你清理下缓存就好了。这篇文章我们就细细的来挖掘下缓存的种种轶事。

# 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!!!";
  }
}
1
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";
    }
}
1
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'
    })
  ]
};
1
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);
      })
  );
});
1
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
1
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 "*";
}
1
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">
1
2
3
4
5
6
7
8

# 10. 常见问题及解决方案

# 10.1 缓存更新不及时

问题:用户访问网站时看到的是旧版本内容

解决方案

  • 使用文件名hash策略
  • 设置合理的缓存时间
  • 在关键资源上使用版本号

# 10.2 缓存穿透

问题:大量请求绕过缓存直接访问服务器

解决方案

  • 使用布隆过滤器过滤无效请求
  • 设置合理的缓存空值策略
  • 限流保护

# 10.3 缓存雪崩

问题:大量缓存在同一时间失效,导致服务器压力骤增

解决方案

  • 设置不同的过期时间
  • 使用互斥锁或分布式锁
  • 缓存预热机制

# 11. 【参考文章】

掘金 -- 缓存详解 (opens new window)

http缓存与cdn缓存配置指南 (opens new window)

你应该知道的浏览器缓存知识 (opens new window)

Google Web Fundamentals - Caching (opens new window)

Last Updated: 9/16/2025, 5:51:53 PM