Skip to content

缓存策略

缓存简介

缓存是提高Web性能的重要手段,通过存储资源副本减少网络请求,加快页面加载速度。

缓存类型

1. 强缓存

浏览器直接从本地缓存读取资源,不向服务器发送请求。

Cache-Control

http
Cache-Control: max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: public
Cache-Control: private
Cache-Control: must-revalidate
Cache-Control: immutable

Expires

http
Expires: Wed, 21 Oct 2024 07:28:00 GMT

示例

javascript
// 设置强缓存
fetch('https://example.com/style.css', {
  headers: {
    'Cache-Control': 'max-age=3600'
  }
});

2. 协商缓存

浏览器向服务器发送请求,服务器判断资源是否修改,返回304状态码或新资源。

ETag

http
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Last-Modified

http
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

示例

javascript
// 协商缓存
fetch('https://example.com/data.json', {
  headers: {
    'If-None-Match': '"33a64df551425fcc55e4d42a148795d9f25f89d4"'
  }
})
  .then(response => {
    if (response.status === 304) {
      console.log('使用缓存');
    } else {
      return response.json();
    }
  });

缓存策略

1. 不缓存

http
Cache-Control: no-store, no-cache, must-revalidate

适用于:

  • 敏感数据
  • 实时数据
  • 频繁变化的内容

2. 短期缓存

http
Cache-Control: max-age=60

适用于:

  • 新闻
  • 社交媒体
  • 实时更新

3. 长期缓存

http
Cache-Control: max-age=31536000, immutable

适用于:

  • 静态资源(CSS、JS、图片)
  • 版本化资源

4. 协商缓存

http
Cache-Control: no-cache
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

适用于:

  • HTML文件
  • API响应
  • 动态内容

Service Worker缓存

基本用法

javascript
// 注册Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker注册成功');
    })
    .catch(error => {
      console.error('Service Worker注册失败:', error);
    });
}

缓存策略

javascript
// sw.js
const CACHE_NAME = 'my-cache-v1';
const urlsToCache = [
  '/',
  '/style.css',
  '/script.js',
  '/image.jpg'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        return cache.addAll(urlsToCache);
      })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        if (response) {
          return response;
        }
        return fetch(event.request);
      })
  );
});

缓存优先

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        return response || fetch(event.request);
      })
  );
});

网络优先

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        const responseClone = response.clone();
        caches.open(CACHE_NAME)
          .then(cache => {
            cache.put(event.request, responseClone);
          });
        return response;
      })
      .catch(() => {
        return caches.match(event.request);
      })
  );
});

缓存回退

javascript
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        return response || fetch(event.request)
          .then(response => {
            const responseClone = response.clone();
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseClone);
              });
            return response;
          })
          .catch(() => {
            return caches.match('/offline.html');
          });
      })
  );
});

浏览器缓存

LocalStorage缓存

javascript
class Cache {
  constructor(prefix = 'cache_') {
    this.prefix = prefix;
  }

  set(key, value, ttl = 3600) {
    const data = {
      value: value,
      expires: Date.now() + ttl * 1000
    };
    localStorage.setItem(this.prefix + key, JSON.stringify(data));
  }

  get(key) {
    const data = localStorage.getItem(this.prefix + key);
    if (!data) {
      return null;
    }
    const parsed = JSON.parse(data);
    if (Date.now() > parsed.expires) {
      this.remove(key);
      return null;
    }
    return parsed.value;
  }

  remove(key) {
    localStorage.removeItem(this.prefix + key);
  }

  clear() {
    const keys = Object.keys(localStorage);
    keys.forEach(key => {
      if (key.startsWith(this.prefix)) {
        localStorage.removeItem(key);
      }
    });
  }
}

const cache = new Cache();
cache.set('user', { name: 'John', age: 30 }, 3600);
const user = cache.get('user');
console.log(user);

IndexedDB缓存

javascript
class IndexedDBCache {
  constructor(dbName, storeName) {
    this.dbName = dbName;
    this.storeName = storeName;
    this.db = null;
  }

  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);

      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onerror = () => {
        reject(request.error);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          const objectStore = db.createObjectStore(this.storeName, { keyPath: 'key' });
          objectStore.createIndex('expires', 'expires', { unique: false });
        }
      };
    });
  }

  async set(key, value, ttl = 3600) {
    const transaction = this.db.transaction([this.storeName], 'readwrite');
    const objectStore = transaction.objectStore(this.storeName);

    const data = {
      key: key,
      value: value,
      expires: Date.now() + ttl * 1000
    };

    return new Promise((resolve, reject) => {
      const request = objectStore.put(data);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  async get(key) {
    const transaction = this.db.transaction([this.storeName], 'readonly');
    const objectStore = transaction.objectStore(this.storeName);

    return new Promise((resolve, reject) => {
      const request = objectStore.get(key);
      request.onsuccess = () => {
        const data = request.result;
        if (!data) {
          resolve(null);
          return;
        }
        if (Date.now() > data.expires) {
          this.remove(key);
          resolve(null);
          return;
        }
        resolve(data.value);
      };
      request.onerror = () => reject(request.error);
    });
  }

  async remove(key) {
    const transaction = this.db.transaction([this.storeName], 'readwrite');
    const objectStore = transaction.objectStore(this.storeName);

    return new Promise((resolve, reject) => {
      const request = objectStore.delete(key);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
}

const cache = new IndexedDBCache('CacheDB', 'cache');
cache.open().then(() => {
  return cache.set('user', { name: 'John', age: 30 }, 3600);
}).then(() => {
  return cache.get('user');
}).then(user => {
  console.log(user);
});

缓存优化

1. 版本化资源

html
<!-- 不好的做法 -->
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>

<!-- 好的做法 -->
<link rel="stylesheet" href="style.css?v=1.0.0">
<script src="script.js?v=1.0.0"></script>

2. 内容哈希

html
<!-- 使用内容哈希 -->
<link rel="stylesheet" href="style.abc123.css">
<script src="script.def456.js"></script>

3. CDN缓存

html
<!-- 使用CDN -->
<script src="https://cdn.example.com/jquery.min.js"></script>

4. 预加载

html
<!-- 预加载资源 -->
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="script.js" as="script">

面试常见问题

1. 强缓存和协商缓存的区别?

  • 强缓存: 浏览器直接从本地缓存读取资源
  • 协商缓存: 浏览器向服务器发送请求,服务器判断资源是否修改

2. Cache-Control的常用值?

http
Cache-Control: max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: public
Cache-Control: private

3. ETag和Last-Modified的区别?

  • ETag: 资源的唯一标识,更精确
  • Last-Modified: 资源的最后修改时间,精度较低

4. Service Worker的缓存策略?

  • 缓存优先
  • 网络优先
  • 缓存回退

5. 如何优化缓存?

  • 版本化资源
  • 使用内容哈希
  • 使用CDN
  • 预加载资源

通过理解缓存策略的各种技术和最佳实践,可以更好地优化Web性能。

好好学习,天天向上