HTTP缓存
HTTP缓存
一、缓存及其优点
缓存的概念
缓存是一种可以自动保存常见资源副本并可以在下一次请求中直接使用副本而非再次获取的技术。
当我们首次进行资源请求之后,服务器在返回资源给客户端的同时,缓存服务器或本地缓存也会保存一份资源副本(在允许缓存的情况下),当下次再对该资源进行请求时,会直接使用资源副本而不会从原始服务器再次请求文档。
缓存的优点
缓存为Web应用带来了显著的性能提升,主要体现在以下几个方面:
- 缓存可以减少冗余的数据传输
- 缓存可以缓解网络瓶颈的问题
- 缓存可以降低对原始服务器的要求
- 缓存可以降低请求的距离时延
减少冗余数据传输
当多个客户端请求相同的资源时,如果没有缓存,原始服务器需要一遍又一遍地传输相同的内容,造成大量数据冗余传输。通过缓存,可以大幅减少这种重复传输,节省带宽资源。
缓解网络瓶颈问题
在大部分情况下,客户端访问代理服务器的速度总是比访问原始服务器更快(带宽大、延迟低),因此如果代理服务器能够提供一份完整的副本,则远远比从原始服务器获取来的快且省流量——尤其针对大文件来说。
降低原始服务器负载
突发事件(比如爆炸性新闻、某个名人事件)可能导致大量用户几乎同时访问某个Web资源,形成瞬间流量高峰。这种情况下,过高的并发请求可能会使网络和Web服务器崩溃。使用缓存可以有效分散负载,降低对原始服务器的压力。
降低请求距离时延
物理距离是影响Web性能的重要因素之一。对于同一份资源,原服务器离请求端越近,资源的获取速度越快。缓存服务器通常分布在不同地理位置,可以为用户提供更近距离的资源访问点,从而降低网络延迟。
二、强缓存和协商缓存
1、缓存相关概念解释
缓存命中
如果某个请求的结果是由已缓存的副本提供的,被称作缓存命中。这意味着客户端可以直接从缓存获取资源,无需与原始服务器通信。
缓存未命中
如果缓存中没有可用的副本或者副本已经过期,则会将请求转发至原始服务器,这被称作缓存未命中。
新鲜度检测
HTTP通过缓存将服务器文档的副本保留一段时间。在这段时间里,都认为文档是"新鲜的",缓存可以在不联系服务器的情况下,直接提供该文档。但一旦已缓存副本停留的时间太长,超过了文档的新鲜度限值(freshness limit),就认为对象"过时"了,在提供该文档之前,缓存要再次与服务器进行确认,以查看文档是否发生了变化。
再验证
原始服务器上的内容可能会随时变化,缓存需要经常对其进行检测,看看它保存的副本是否仍是服务器上最新的副本。这些新鲜度检测被称为HTTP再验证。
缓存可以随时对副本进行再验证,但大部分缓存只在客户端发起请求,并且副本旧得足以需要检测的时候,才会对副本进行再验证。
再验证命中和再验证未命中
缓存对缓存的副本进行再验证时,会向原始服务器发送一个再验证请求:
- 如果内容没有发生变化,服务器会以304 Not Modified进行响应。这被称作是再验证命中或者缓慢命中。
- 如果内容发生了变化,服务器会以200 OK进行响应,并返回新内容。这被称作再验证未命中。
2、缓存的处理步骤
HTTP缓存的处理流程如下:
- 当用户请求资源时,首先判断是否有缓存
- 如果没有缓存,则向原服务器请求资源
- 如果有缓存,进入下一步判断
- 强缓存判断
- 检查缓存是否新鲜(通过Expires或Cache-Control: max-age判断)
- 如果缓存新鲜,直接返回缓存副本给客户端(强缓存命中)
- 如果缓存不新鲜,进入协商缓存阶段
- 协商缓存判断
- 检查是否存在Etag或Last-Modified首部
- 向服务器发送带有If-None-Match或If-Modified-Since的请求
- 服务器根据这些首部验证资源是否发生变化
- 如果资源未变化,返回304状态码(协商缓存命中)
- 如果资源已变化,返回200状态码和新资源(协商缓存未命中)
3、强缓存和协商缓存的概念
强缓存
强缓存是指浏览器直接从本地缓存中获取资源,而不与服务器进行任何通信。
当首次发起请求时,服务端会在Response Headers中写入缓存新鲜时间(通过Expires或Cache-Control: max-age)。当再次请求该资源时,如果缓存仍然新鲜,浏览器将直接从缓存获取资源,不会与服务器发生任何通信。
强缓存命中时,浏览器开发者工具中通常会显示200 (from disk cache)**或**200 (from memory cache)。
协商缓存
协商缓存是指浏览器需要向服务器询问缓存的相关信息,由服务器决定是否使用缓存。
在协商缓存机制下,浏览器必须先向服务器发送请求,与服务器协商判断缓存是否仍然有效。服务器会根据请求头中的特定字段(如If-Modified-Since或If-None-Match)决定是返回完整资源(200),还是告知浏览器使用本地缓存(304)。
4、强缓存和协商缓存的实现原理
(1)强缓存实现原理
强缓存主要通过两个HTTP头部字段实现:Expires和Cache-Control: max-age。
Expires(HTTP/1.0)
Expires描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示。例如:
Expires: Wed, 21 Oct 2025 07:28:00 GMT由于Expires是一个绝对时间,如果客户端的时间设置不准确或被人为修改,会对缓存有效期造成影响,使缓存控制失效。因此在HTTP/1.1中引入了Cache-Control: max-age作为Expires的替代方案。
Cache-Control(HTTP/1.1)
Cache-Control是HTTP/1.1引入的更灵活的缓存控制机制,其中max-age值定义了资源的最大有效期(以秒为单位)。例如:
Cache-Control: max-age=31536000上面的配置表示资源在31536000秒(一年)内均可使用缓存,无需向服务器再次请求。
过程详解:
- 首次请求资源时,服务器在返回资源的同时,会在Response Headers中写入Expires或Cache-Control标识缓存的过期时间。
- 再次请求该资源时,浏览器会检查本地缓存是否过期:
- 对于Expires,浏览器会将当前时间与Expires指定的时间比较
- 对于Cache-Control: max-age,浏览器会计算资源的获取时间与当前时间的差值,与max-age比较
- 如果缓存未过期,浏览器直接使用缓存资源,不会发送网络请求
- 如果缓存已过期,浏览器会进入协商缓存流程
(2)协商缓存实现原理
协商缓存主要通过两对HTTP头部字段实现:Last-Modified/If-Modified-Since和ETag/If-None-Match。
Last-Modified/If-Modified-Since
Last-Modified标识资源在服务器上的最后修改时间。实现流程如下:
首次请求资源时,服务器在返回资源的同时,会在Response Headers中写入Last-Modified首部,例如:
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT当再次请求该资源且缓存已过期时,浏览器会在Request Headers中添加If-Modified-Since首部,其值为之前收到的Last-Modified值:
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT服务器接收到请求后,会比较If-Modified-Since的值与资源的当前最后修改时间:
- 如果资源未更新(最后修改时间未变),返回304 Not Modified响应,不包含资源内容
- 如果资源已更新,返回200 OK响应和完整的资源内容,同时更新Last-Modified值
资源未更新的网络请求示例:
首次请求响应头:
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Content-Type: text/html
Content-Length: 1024再次请求头:
GET /resource HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT再次请求响应头:
HTTP/1.1 304 Not Modified
Date: Thu, 22 Oct 2024 07:28:00 GMTETag/If-None-Match
ETag(Entity Tag)是服务器为每个资源生成的唯一标识字符串,基于资源内容编码生成。实现流程如下:
首次请求资源时,服务器在返回资源的同时,会在Response Headers中写入ETag首部,例如:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"当再次请求该资源且缓存已过期时,浏览器会在Request Headers中添加If-None-Match首部,其值为之前收到的ETag值:
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"服务器接收到请求后,会比较If-None-Match的值与资源的当前ETag:
- 如果两者匹配(资源未变化),返回304 Not Modified响应
- 如果两者不匹配(资源已变化),返回200 OK响应和完整的资源内容,同时更新ETag值
ETag强验证器和弱验证器
ETag分为强验证器和弱验证器:
强验证器:要求资源的每个字节都相同才认为资源未变化
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"弱验证器:只要求资源的语义相同(即使有微小差异)就认为资源未变化,使用W/前缀标识
ETag: W/"33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified的局限性
Last-Modified存在以下几个问题:
- 无法感知内容变化:如果文件被修改但内容实际没变(如重写但内容相同),Last-Modified也会更新,导致不必要的资源传输
- 精度限制:Last-Modified的精度只到秒,无法感知一秒内的多次修改
- 服务器时间同步问题:如果服务器之间时间不同步,可能导致缓存失效
ETag解决了这些问题,它基于文件内容而非修改时间,能够更准确地判断资源是否真正发生变化,因此ETag通常被视为比Last-Modified更优的选择。
5、Cache-Control详解
Cache-Control是HTTP/1.1中引入的缓存控制头部,提供了丰富的缓存策略选项:
max-age/s-maxage
max-age:指定资源在客户端缓存的最大有效期(秒)
Cache-Control: max-age=3600 // 资源在客户端缓存1小时s-maxage:指定资源在共享缓存(如CDN)中的最大有效期(秒)
Cache-Control: s-maxage=7200 // 资源在CDN缓存2小时
s-maxage仅适用于共享缓存,且优先级高于max-age。如果同时设置,私有缓存遵循max-age,共享缓存遵循s-maxage。
public/private
public:表示资源可以被任何缓存存储,包括浏览器缓存和共享缓存(如CDN)
Cache-Control: public, max-age=3600private:表示资源仅可被浏览器等私有缓存存储,不能被CDN等共享缓存存储
Cache-Control: private, max-age=3600
通常包含用户个人信息的响应应该设置为private。
no-cache/no-store
no-cache:不是不缓存,而是强制进行协商缓存。浏览器会缓存资源,但每次使用前必须与服务器验证是否有效
Cache-Control: no-cacheno-store:完全禁止缓存,浏览器不会存储任何版本的资源
Cache-Control: no-store
no-store通常用于敏感信息(如银行数据、个人隐私信息等)。
must-revalidate
指示一旦资源过期(即超过max-age),在成功向原始服务器验证之前,不能使用缓存响应
Cache-Control: max-age=3600, must-revalidateimmutable
表示资源内容永远不会改变,因此客户端不应该发送任何条件请求(即使刷新页面)
Cache-Control: max-age=31536000, immutable这对于带有版本号或哈希值的静态资源(如JavaScript、CSS文件)特别有用。
6、优先级问题
(1)Expires 和 Cache-Control: max-age
当Expires和Cache-Control: max-age同时存在时:
- HTTP/1.1客户端会优先使用max-age,忽略Expires
- HTTP/1.0客户端会忽略max-age,使用Expires
为了兼容性,最好同时设置这两个头部,但确保它们指定的过期时间一致。
(2)Last-Modified 和 ETag
当Last-Modified和ETag同时存在时,根据HTTP规范,服务器应该先验证ETag,再验证Last-Modified:
- 首先检查ETag(If-None-Match):
- 如果ETag不匹配,表示资源已变化,直接返回200和新资源
- 如果ETag匹配,继续下一步
- 然后检查Last-Modified(If-Modified-Since):
- 如果Last-Modified表明资源已更新,返回200和新资源
- 如果Last-Modified表明资源未更新,返回304
这种双重验证提供了更可靠的缓存控制,避免了单一验证方式的局限性。
三、缓存决策与最佳实践
缓存策略选择
选择适当的缓存策略取决于资源的特性和更新频率:
1. 永不缓存
对于频繁变化或包含敏感信息的资源,可以完全禁止缓存:
Cache-Control: no-store适用场景:银行交易数据、实时股票价格、用户认证页面等。
2. 验证缓存但每次都需要检查
对于经常变化但可以缓存的资源,强制进行协商缓存:
Cache-Control: no-cache适用场景:新闻页面、社交媒体feed、搜索结果等。
3. 短期缓存
对于有一定时效性的资源,设置较短的缓存期:
Cache-Control: max-age=300, must-revalidate适用场景:天气信息、热门文章列表等(缓存5分钟)。
4. 长期缓存
对于不经常变化的静态资源,设置较长的缓存期并使用版本控制:
Cache-Control: max-age=31536000, immutable适用场景:带有版本号或哈希值的JS/CSS文件、图片、字体等(缓存1年)。
版本化静态资源
对于静态资源,最佳实践是在文件名或URL中加入版本号或内容哈希:
styles.v2.css
main.a7e3d9c.js
image.png?v=1234这样,当资源内容更新时,URL也会变化,客户端会请求新版本,而旧版本仍可以长期缓存。
四、前端缓存实践与应用
1. Service Worker缓存
Service Worker是一种运行在浏览器背后的脚本,可以实现高级缓存和离线功能。
基本用法示例:
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker注册成功:', registration.scope);
})
.catch(error => {
console.log('ServiceWorker注册失败:', error);
});
}
// sw.js文件内容
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
// 安装Service Worker并缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
);
});
// 拦截请求并从缓存提供资源
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中,返回缓存的资源
if (response) {
return response;
}
// 缓存未命中,从网络获取
return fetch(event.request).then(
response => {
// 检查响应是否有效
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应(因为响应只能使用一次)
const responseToCache = response.clone();
// 将新获取的资源添加到缓存
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
// 清理旧缓存
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});2. localStorage和sessionStorage缓存
浏览器存储API可用于缓存小型数据。
localStorage示例(持久化存储):
// 缓存数据
function cacheData(key, data, expirationInMinutes = 60) {
const now = new Date();
const item = {
value: data,
expiry: now.getTime() + expirationInMinutes * 60 * 1000
};
localStorage.setItem(key, JSON.stringify(item));
}
// 获取缓存数据
function getCachedData(key) {
const itemStr = localStorage.getItem(key);
// 如果不存在该键,返回null
if (!itemStr) {
return null;
}
const item = JSON.parse(itemStr);
const now = new Date();
// 检查是否过期
if (now.getTime() > item.expiry) {
// 如果过期,删除该项并返回null
localStorage.removeItem(key);
return null;
}
return item.value;
}
// 使用示例
function fetchUserData(userId) {
// 尝试从缓存获取
const cachedData = getCachedData(`user_${userId}`);
if (cachedData) {
console.log('从缓存获取数据');
return Promise.resolve(cachedData);
}
// 缓存未命中,从API获取
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
// 缓存数据(30分钟)
cacheData(`user_${userId}`, data, 30);
return data;
});
}sessionStorage示例(会话期间存储):
// 保存会话数据
function saveSessionData(key, data) {
sessionStorage.setItem(key, JSON.stringify(data));
}
// 获取会话数据
function getSessionData(key) {
const data = sessionStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
// 使用示例 - 保存表单状态
const form = document.getElementById('myForm');
// 保存表单数据
form.addEventListener('input', () => {
const formData = {
name: form.elements.name.value,
email: form.elements.email.value,
message: form.elements.message.value
};
saveSessionData('form_draft', formData);
});
// 页面加载时恢复表单数据
document.addEventListener('DOMContentLoaded', () => {
const savedForm = getSessionData('form_draft');
if (savedForm) {
form.elements.name.value = savedForm.name || '';
form.elements.email.value = savedForm.email || '';
form.elements.message.value = savedForm.message || '';
}
});3. Memory Cache(内存缓存)管理
使用JavaScript实现一个简单的内存缓存系统:
class MemoryCache {
constructor(maxSize = 100) {
this.cache = {};
this.maxSize = maxSize;
this.size = 0;
this.hits = 0;
this.misses = 0;
}
// 设置缓存项
set(key, value, ttl = 60000) { // 默认60秒
// 如果缓存已满,清除最早的项
if (this.size >= this.maxSize) {
this._removeOldest();
}
const item = {
value,
expiry: Date.now() + ttl,
timestamp: Date.now()
};
// 如果键已存在,不增加size
if (!this.cache[key]) {
this.size++;
}
this.cache[key] = item;
return true;
}
// 获取缓存项
get(key) {
const item = this.cache[key];
// 未找到项
if (!item) {
this.misses++;
return null;
}
// 项已过期
if (Date.now() > item.expiry) {
this._remove(key);
this.misses++;
return null;
}
this.hits++;
return item.value;
}
// 删除缓存项
delete(key) {
return this._remove(key);
}
// 清空缓存
clear() {
this.cache = {};
this.size = 0;
return true;
}
// 获取缓存统计信息
getStats() {
return {
size: this.size,
maxSize: this.maxSize,
hits: this.hits,
misses: this.misses,
hitRatio: this.hits / (this.hits + this.misses) || 0
};
}
// 内部方法:移除最早添加的项
_removeOldest() {
let oldest = null;
let oldestKey = null;
for (const key in this.cache) {
const item = this.cache[key];
if (oldest === null || item.timestamp < oldest) {
oldest = item.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this._remove(oldestKey);
}
}
// 内部方法:移除指定键
_remove(key) {
if (this.cache[key]) {
delete this.cache[key];
this.size--;
return true;
}
return false;
}
}
// 使用示例
const apiCache = new MemoryCache(50); // 最多缓存50项
async function fetchWithCache(url, options = {}) {
const cacheKey = `${url}-${JSON.stringify(options)}`;
// 尝试从缓存获取
const cachedResponse = apiCache.get(cacheKey);
if (cachedResponse) {
console.log('从缓存获取:', url);
return cachedResponse;
}
// 缓存未命中,发起网络请求
console.log('从网络获取:', url);
const response = await fetch(url, options);
const data = await response.json();
// 缓存响应(5分钟)
apiCache.set(cacheKey, data, 5 * 60 * 1000);
return data;
}
// 示例调用
fetchWithCache('https://api.example.com/users')
.then(data => console.log('获取到的数据:', data))
.catch(error => console.error('请求错误:', error));
// 查看缓存统计
console.log(apiCache.getStats());4.资源预加载和预缓存策略
在前端应用中,我们可以通过资源预加载提高用户体验:
/**
* 预加载图片资源并缓存
* @param {Array} imageUrls - 需要预加载的图片URL数组
* @returns {Promise} - 完成预加载的Promise
*/
function preloadImages(imageUrls) {
return Promise.all(imageUrls.map(url => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(url);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
}))
.then(results => {
console.log('预加载完成:', results);
return results;
})
.catch(error => {
console.error('预加载图片出错:', error);
throw error;
});
}
// 使用案例
document.addEventListener('DOMContentLoaded', () => {
// 获取下一页会用到的图片
const nextPageImages = [
'/images/banner.jpg',
'/images/product1.jpg',
'/images/product2.jpg'
];
// 当用户空闲时预加载图片
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
preloadImages(nextPageImages);
});
} else {
// 降级处理
setTimeout(() => {
preloadImages(nextPageImages);
}, 1000);
}
});5. 基于HTTP缓存的前端性能优化
利用HTTP缓存头实现前端性能优化:
/**
* 生成带有版本号的资源URL,用于长期缓存
* @param {string} url - 原始资源URL
* @param {string} version - 版本号或哈希值
* @returns {string} - 带版本号的URL
*/
function versionedUrl(url, version) {
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}v=${version}`;
}
// 应用配置
const appConfig = {
version: '1.2.3',
buildId: '8f4d9c7', // 每次构建自动生成
assets: {
css: [
'/css/main.css',
'/css/components.css'
],
js: [
'/js/app.js',
'/js/vendor.js'
]
}
};
// 为所有静态资源添加版本号
document.addEventListener('DOMContentLoaded', () => {
// 为CSS添加版本号
appConfig.assets.css.forEach(cssFile => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = versionedUrl(cssFile, appConfig.buildId);
document.head.appendChild(link);
});
// 为JS添加版本号
appConfig.assets.js.forEach(jsFile => {
const script = document.createElement('script');
script.src = versionedUrl(jsFile, appConfig.buildId);
script.async = true;
document.body.appendChild(script);
});
// 预加载可能需要的资源
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.as = 'image';
preloadLink.href = versionedUrl('/images/hero.jpg', appConfig.buildId);
document.head.appendChild(preloadLink);
});6. Webpack中的缓存配置
在Webpack构建中可以通过文件名哈希实现有效的缓存控制:
// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
// 使用内容哈希实现长期缓存
filename: '[name].[contenthash].js',
// 为公共路径添加CDN域名
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/'
},
optimization: {
// 提取第三方库到单独的chunk
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
},
// 保持runtime代码独立,避免vendor哈希变化
runtimeChunk: 'single',
// 确保模块ID的稳定性
moduleIds: 'deterministic'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
// 添加缓存控制HTTP头的配置
meta: {
'Cache-Control': {'http-equiv': 'Cache-Control', 'content': 'max-age=31536000'}
}
}),
new MiniCssExtractPlugin({
// 为CSS文件添加内容哈希
filename: '[name].[contenthash].css',
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
generator: {
// 为图片添加内容哈希
filename: 'images/[name].[contenthash][ext]'
}
}
]
}
};五、缓存相关的常见问题与解决方案
1. 缓存穿透问题
问题描述:客户端请求的资源在服务器和缓存中都不存在,导致每次请求都会穿透缓存直达服务器。
解决方案:
/**
* 防止缓存穿透的数据获取函数
* @param {string} key - 缓存键
* @param {Function} fetchData - 获取数据的函数
* @returns {Promise} - 包含数据的Promise
*/
async function getCachedDataWithProtection(key, fetchData) {
const cache = new MemoryCache();
// 尝试从缓存获取
const cachedData = cache.get(key);
if (cachedData !== null) {
return cachedData; // 包括null值
}
try {
// 从数据源获取数据
const data = await fetchData();
// 即使数据为null也缓存结果(防止缓存穿透)
// 对于null值使用较短的过期时间(5分钟)
const ttl = data === null ? 5 * 60 * 1000 : 30 * 60 * 1000;
cache.set(key, data, ttl);
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
// 使用示例
async function fetchUserData(userId) {
return getCachedDataWithProtection(
`user_${userId}`,
async () => {
const response = await fetch(`/api/users/${userId}`);
if (response.status === 404) {
return null; // 用户不存在,返回null但会被缓存
}
if (!response.ok) {
throw new Error(`获取用户数据失败: ${response.status}`);
}
return response.json();
}
);
}2. 缓存更新策略
问题描述:如何保证缓存中的数据与服务器保持同步。
解决方案:
/**
* 实现缓存更新策略的类
*/
class CacheManager {
constructor() {
this.cache = new MemoryCache();
this.subscribers = new Map();
}
/**
* 获取数据(缓存优先策略)
*/
async getData(key, fetchFunc, options = {}) {
const {
ttl = 30 * 60 * 1000, // 默认30分钟
staleWhileRevalidate = true, // 默认启用后台刷新
forceRefresh = false // 默认不强制刷新
} = options;
// 检查是否有订阅者正在刷新该键
if (this.subscribers.has(key)) {
// 等待正在进行的刷新完成
return this.subscribers.get(key);
}
// 如果不是强制刷新,尝试从缓存获取
if (!forceRefresh) {
const cachedData = this.cache.get(key);
if (cachedData) {
// 如果启用了staleWhileRevalidate且接近过期时间
// 在后台刷新缓存但立即返回旧数据
const cacheItem = this.cache.cache[key];
const isNearExpiry = Date.now() > (cacheItem.expiry - (ttl * 0.2));
if (staleWhileRevalidate && isNearExpiry) {
this._refreshInBackground(key, fetchFunc, ttl);
}
return cachedData;
}
}
// 缓存未命中或强制刷新,获取新数据
return this._refresh(key, fetchFunc, ttl);
}
/**
* 刷新数据并更新缓存
*/
async _refresh(key, fetchFunc, ttl) {
// 创建Promise以允许其他请求等待结果
const fetchPromise = (async () => {
try {
const data = await fetchFunc();
this.cache.set(key, data, ttl);
return data;
} catch (error) {
// 请求失败,从缓存获取旧数据
const oldData = this.cache.get(key);
if (oldData !== null) {
console.warn(`刷新数据失败,使用旧数据: ${key}`, error);
return oldData;
}
throw error;
} finally {
// 完成后移除订阅者
this.subscribers.delete(key);
}
})();
// 记录正在进行的刷新
this.subscribers.set(key, fetchPromise);
return fetchPromise;
}
/**
* 在后台刷新数据
*/
_refreshInBackground(key, fetchFunc, ttl) {
// 不等待结果,只在后台执行
this._refresh(key, fetchFunc, ttl).catch(error => {
console.error(`后台刷新失败: ${key}`, error);
});
}
/**
* 使特定键的缓存失效
*/
invalidate(key) {
return this.cache.delete(key);
}
/**
* 清空所有缓存
*/
clearAll() {
return this.cache.clear();
}
}
// 使用示例
const cacheManager = new CacheManager();
// 获取数据(自动处理缓存)
async function getProductList(category) {
return cacheManager.getData(
`products_${category}`,
() => fetch(`/api/products?category=${category}`).then(r => r.json()),
{ ttl: 10 * 60 * 1000 } // 10分钟缓存
);
}
// 提交表单后使相关缓存失效
function submitProductForm(productData) {
return fetch('/api/products', {
method: 'POST',
body: JSON.stringify(productData),
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(result => {
// 使产品列表缓存失效
cacheManager.invalidate(`products_${productData.category}`);
return result;
});
}3. 动态内容的缓存策略
对于动态内容,我们可以采用不同的缓存策略:
/**
* 适用于动态内容的分段缓存策略
*/
class DynamicContentCache {
constructor() {
this.staticCache = new MemoryCache(); // 静态部分缓存
this.dynamicCache = new MemoryCache(); // 动态部分缓存(短期)
}
/**
* 获取页面内容,组合静态和动态部分
*/
async getPageContent(pageId, userData) {
try {
// 并行获取静态和动态内容
const [staticContent, dynamicContent] = await Promise.all([
this._getStaticContent(pageId),
this._getDynamicContent(pageId, userData)
]);
// 组合内容
return {
...staticContent,
...dynamicContent,
isPartiallyFromCache: dynamicContent.fromCache || staticContent.fromCache,
renderedAt: new Date().toISOString()
};
} catch (error) {
console.error('获取页面内容失败:', error);
throw error;
}
}
/**
* 获取静态内容(长期缓存)
*/
async _getStaticContent(pageId) {
const cacheKey = `static_${pageId}`;
let fromCache = false;
// 尝试从缓存获取
let content = this.staticCache.get(cacheKey);
if (content) {
fromCache = true;
} else {
// 从API获取静态内容
const response = await fetch(`/api/pages/${pageId}/static`);
content = await response.json();
// 长期缓存(1天)
this.staticCache.set(cacheKey, content, 24 * 60 * 60 * 1000);
}
return { ...content, fromCache };
}
/**
* 获取动态内容(短期缓存)
*/
async _getDynamicContent(pageId, userData) {
// 对于个性化内容,使用用户ID创建唯一缓存键
const userId = userData?.id || 'anonymous';
const cacheKey = `dynamic_${pageId}_${userId}`;
let fromCache = false;
// 尝试从缓存获取
let content = this.dynamicCache.get(cacheKey);
if (content) {
fromCache = true;
} else {
// 从API获取动态内容
const response = await fetch(`/api/pages/${pageId}/dynamic`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
content = await response.json();
// 短期缓存(5分钟)
this.dynamicCache.set(cacheKey, content, 5 * 60 * 1000);
}
return { ...content, fromCache };
}
/**
* 使页面缓存失效
*/
invalidatePage(pageId, userId = null) {
// 使静态内容失效
this.staticCache.delete(`static_${pageId}`);
if (userId) {
// 使特定用户的动态内容失效
this.dynamicCache.delete(`dynamic_${pageId}_${userId}`);
} else {
// 使所有用户的该页面动态内容失效
// (简单实现,实际可能需要正则匹配或前缀扫描)
Object.keys(this.dynamicCache.cache).forEach(key => {
if (key.startsWith(`dynamic_${pageId}_`)) {
this.dynamicCache.delete(key);
}
});
}
}
}
// 使用示例
const pageCache = new DynamicContentCache();
// 获取包含个性化内容的页面
async function renderProductPage(productId, user) {
const pageContent = await pageCache.getPageContent(`product_${productId}`, user);
// 渲染页面
document.title = pageContent.title;
document.getElementById('product-description').innerHTML = pageContent.description;
document.getElementById('recommendations').innerHTML = pageContent.recommendations.map(item =>
`<div class="product-card">${item.name} - $${item.price}</div>`
).join('');
// 显示缓存状态
if (pageContent.isPartiallyFromCache) {
console.log('页面部分内容来自缓存');
}
}六、总结与最佳实践
缓存策略决策树
在设计Web应用的缓存策略时,可以遵循以下决策树:
- 资源是否需要实时性?
- 是 → 使用
no-store或no-cache - 否 → 继续下一步
- 是 → 使用
- 资源是否包含敏感信息?
- 是 → 使用
no-store或private, no-cache - 否 → 继续下一步
- 是 → 使用
- 资源是否会频繁更新?
- 是 → 使用协商缓存(ETag/Last-Modified)
- 否 → 继续下一步
- 资源是否有版本控制?
- 是 → 使用长期缓存(
max-age=31536000, immutable) - 否 → 使用适当的
max-age(如1小时到1天)并确保有协商缓存机制
- 是 → 使用长期缓存(
最佳实践总结
- 静态资源优化
- 为静态资源添加版本号或内容哈希
- 使用长期缓存(
Cache-Control: max-age=31536000, immutable) - 将CSS和JavaScript拆分为核心和非核心部分
- API响应缓存
- 对读取操作使用适当的缓存策略
- 对更新操作后主动使相关缓存失效
- 使用ETag进行精确的资源变化检测
- 资源预加载
- 使用
<link rel="preload">预加载关键资源 - 使用Service Worker预缓存应用核心资源
- 在用户空闲时预加载可能需要的资源
- 使用
- 缓存问题处理
- 实施防止缓存穿透的措施
- 使用stale-while-revalidate模式提高性能
- 为动态内容实施分段缓存策略
- 缓存监控
- 监控缓存命中率
- 分析缓存效率并持续优化
- 设置缓存相关的性能指标
HTTP缓存是前端性能优化的基础,合理利用浏览器缓存机制可以显著提升用户体验。通过本文介绍的缓存原理和前端实践,希望能帮助开发者更好地理解和应用HTTP缓存,打造高性能的Web应用。
参考资料
浅谈HTTP缓存也就是说,当我们首次进行资源请求之后,服务器在返回资源给客户端的同时,缓存服务器或本地缓存也会保存一份资 - 掘金