缓存(Cache)是一种优化技术,通过将数据(如网络响应、计算结果、静态资源等)临时存储在更靠近用户或计算单元的位置(例如浏览器内存、硬盘、CDN 节点),以便在后续请求或访问时能够更快地获取。
在前端领域,缓存的核心目标是:
简单来说,缓存就是用空间(存储)换时间(速度),是前端性能优化的关键手段之一。本文将系统性地探讨前端涉及的各种缓存机制。
前端缓存涉及多个层面和技术栈。根据缓存的位置、实现方式和控制策略,我们可以识别出几种在前端开发中至关重要的缓存类型:
Cache-Control
、Expires
、ETag/Last-Modified
Service Worker Cache API
(离线/策略缓存)Redux/Context
、localStorage
、IndexedDB
石器时代:无缓存(1989-1993)
最早的 HTTP/0.9 和 HTTP/1.0 初期,浏览器就像一个健忘的老人,每次看到同一张照片都要惊呼"哇,这是谁啊?"——每次请求都重新下载,无论资源是否变化。想象一下,每次刷新页面,你的调制解调器都要发出那段经典的"滋滋啦啦"连接声,用 28.8kbps 的速度重新下载整个页面...痛苦!
青铜时代:Expires(1994-1996)
HTTP/1.0 引入了 Expires 头,这是缓存界的"原始人发现火种"时刻!服务器告诉浏览器:"这个资源在 XX 时间前都是新鲜的,不用再问我了!"
Expires: Wed, 21 Oct 2025 07:28:00 GMT
但 Expires 有个致命问题:它用的是绝对时间。想象一下,如果你的电脑时间不准(或者你是时间旅行者),缓存立刻就失效了!更糟的是,服务器和客户端时区不同时,简直就是一场"时间混战"。
铁器时代:Cache-Control(1997-2000)
HTTP/1.1 带来了革命性的 Cache-Control,它使用相对时间,完美解决了时区问题!
Cache-Control: max-age=3600
这就像告诉浏览器:"这个资源在接下来的一小时内都是新鲜的,无需再问我!"
Cache-Control 还带来了一系列指令,让缓存控制变得更精细:
工业时代:协商缓存(1997-2005)
仅仅知道资源是否过期还不够,我们需要更高效的方式来验证资源是否真的变了。于是,协商缓存机制诞生了!
Last-Modified/If-Modified-Since
最早的协商机制基于时间戳:
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
浏览器下次请求时会带上:
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
这就像问:"嘿,自从我上次看到这个资源后,它变了吗?"如果没变,服务器只需回复一个轻量级的 304 状态码,不用再传输整个资源。
但时间戳有个问题:如果资源在 1 秒内修改了多次,或者修改后内容实际上没变(比如自动保存),这种机制就不够精确了。
ETag/If-None-Match
为解决这个问题,ETag(实体标签)应运而生:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
浏览器下次请求时:
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
ETag
通常是内容的哈希值,只要内容变化,哈希就会变,无论修改时间多接近。这就像给资源的每个版本一个独特的"指纹",比时间戳精确得多!
信息时代:离线优先革命(2014-2017)
传统 HTTP 缓存有个致命弱点:它完全由服务器控制,客户端只能被动接受。如果你想让用户在飞机上也能浏览你的网站,传统缓存就无能为力了。
2014 年,Service Worker 和 Cache API 出现了,这是缓存史上的"量子跃迁"!
// 在 Service Worker 安装时预缓存关键资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
]);
})
);
});
这让开发者第一次能够完全控制缓存策略,实现真正的"离线优先"体验!你可以:
人工智能时代:智能缓存策略(2018-至今)
随着 Workbox 等库的出现,Service Worker 缓存变得更加智能和易用:
// Workbox 示例:不同资源使用不同策略
workbox.routing.registerRoute(
/\.(?:js|css)$/,
new workbox.strategies.StaleWhileRevalidate()
);
workbox.routing.registerRoute(
/\.(?:png|jpg|jpeg|svg|gif)$/,
new workbox.strategies.CacheFirst()
);
workbox.routing.registerRoute(/api/, new workbox.strategies.NetworkFirst());
这就像给你的网站配备了一个"AI 管家",为不同类型的资源自动应用最佳缓存策略!
全球化时代:边缘计算(1998-2010)
想象一下,如果你的服务器在美国,但用户在中国,每个请求都要跨越太平洋...这简直是数字时代的"环球旅行"!
CDN(内容分发网络)通过在全球部署缓存服务器,彻底改变了这一局面:
用户 → 最近的CDN节点 → [如果未缓存] → 源服务器
早期的 CDN 主要缓存静态资源(图片、CSS、JS),使用标准的 HTTP 缓存头来控制缓存行为。
云计算时代:智能 CDN(2010-至今)
现代 CDN 不再只是被动缓存,而是成为了可编程的"边缘计算平台":
// Cloudflare Workers 示例
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// 自定义缓存逻辑
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
response = await fetch(request);
// 自定义缓存条件
if (response.status === 200) {
await cache.put(request, response.clone());
}
}
return response;
}
现代 CDN 甚至可以:
早期浏览器时代:DOM 缓存(1995-2005)
最早的"前端缓存"其实是开发者手动操作 DOM:
// 2000年代的"缓存"
var userNameElement = document.getElementById('username'); // 缓存DOM引用
function updateUsername() {
userNameElement.textContent = '新用户名'; // 避免重复查询DOM
}
Web 2.0 时代:本地存储(2005-2015)
随着 AJAX 的兴起,我们需要在客户端存储更多数据:
Cookie(最古老但限制多)
document.cookie = 'username=John; expires=Thu, 18 Dec 2025 12:00:00 UTC';
Cookie 就像 90 年代的老式硬盘:容量小(4KB)、速度慢(每次请求都会发送)。
LocalStorage(2009 年 HTML5 带来的福音)
localStorage.setItem('name', 'Zilin');
const name = localStorage.getItem('name');
LocalStorage 就像一个简单但可靠的文件柜:容量大(5MB)、永久存储,但只能存字符串。
IndexedDB(2010 年代的客户端数据库)
const request = indexedDB.open('MyDatabase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
// 使用数据库存储复杂数据
};
IndexedDB 就像在浏览器中塞了一个小型数据库,可以存储几乎任何类型的数据,甚至支持索引和事务!
现代前端时代:状态管理(2015-至今)
随着 React/Vue 等框架的兴起,内存中的状态管理成为新的"缓存层":
// Redux示例
const userReducer = (state = {}, action) => {
switch (action.type) {
case 'FETCH_USER_SUCCESS':
return action.payload; // 缓存用户数据
default:
return state;
}
};
这些状态管理工具本质上是应用级的内存缓存,它们:
React Query/SWR(2020-至今)
最新的数据获取库将 HTTP 缓存和状态管理完美结合:
// React Query示例
const { data, isLoading } = useQuery('userData', fetchUserData, {
staleTime: 60000, // 数据保鲜期1分钟
cacheTime: 5 * 60000, // 缓存保留5分钟
refetchOnWindowFocus: true, // 窗口聚焦时重新验证
});
这些库提供了:
缓存的未来:边缘渲染与 AI 预测(2025-?)
随着边缘计算和 AI 的发展,未来的缓存可能会:
从最初的简单 Expires 头,到今天复杂的多层缓存架构,前端缓存已经走过了漫长的进化之路。每一次技术革新,都让我们的应用更快、更可靠、更智能!
专业的 HTTP 缓存规则可以从 HTTP Caching - RFC9111 中找到。
HTTP缓存是前端缓存体系的基础,它由浏览器根据服务器返回的响应头自动管理。理解HTTP缓存机制对优化网站性能至关重要。
HTTP缓存的工作流程可分为以下几个关键步骤:
缓存检查:浏览器发起请求前,首先检查本地缓存库(内存缓存或磁盘缓存)中是否有匹配的响应。
缓存新鲜度验证:若找到缓存,则根据缓存控制头判断缓存是否"新鲜":
Cache-Control: max-age=<seconds>
计算剩余生命周期Expires
与当前时间比较no-store
,则完全禁止缓存,必须发起网络请求no-cache
,则缓存需要经过服务器再验证才能使用强缓存命中:如果缓存新鲜,浏览器直接使用本地缓存,不与服务器通信。这种情况下,网络面板会显示 200 (from disk cache)
或 200 (from memory cache)
。
协商缓存验证:如果缓存已过期或标记为需要验证,浏览器发起"条件请求":
If-None-Match: <etag>
头(对应服务器之前返回的 ETag
值)If-Modified-Since: <date>
头(对应服务器之前返回的 Last-Modified
值)304 Not Modified
,表示资源未变化,浏览器可继续使用本地缓存(但会更新缓存的新鲜度信息)200 OK
及完整内容,表示资源已更新,浏览器使用新响应并更新缓存服务器响应头:
Cache-Control: max-age=3600, public
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
客户端请求头:
Cache-Control: max-age=0
Cache-Control: no-cache
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
特性 | 强缓存 | 协商缓存 |
---|---|---|
是否与服务器通信 | 否 | 是(轻量级验证) |
响应码 | 200 (from cache) | 304 Not Modified |
控制头 | Cache-Control, Expires | ETag, Last-Modified |
适用场景 | 稳定资源(如带hash的JS/CSS) | 可能变化的资源(如HTML) |
性能优势 | 最佳(完全避免网络请求) | 较好(避免响应体传输) |
Cache-Control: max-age=31536000, immutable
Cache-Control: no-cache
ETag: "..."
Cache-Control: max-age=60, private // 短期私有缓存
// 或
Cache-Control: no-store // 敏感数据禁止缓存
s-maxage
区分CDN与浏览器缓存时间Cache-Control: max-age=600, s-maxage=3600
通过精细控制HTTP缓存策略,可以在保证内容及时更新的同时,最大限度减少不必要的网络请求,提升用户体验和页面性能。
下图展示了完整的HTTP缓存决策流程:
下面我们一步一步、用最简单的比喻来讲解 Service Worker 缓存
,假设你只懂最基础的 HTML/JS。
CacheStorage
就像你家门口的储物柜,有很多带名字的箱子(Cache)// 在 Service Worker 里,你先向大仓库拿到/创建一个叫 v1 的箱子
const cache = await caches.open('v1');
caches
→ 全局的储物柜(CacheStorage)open('v1')
→ 如果柜里没有"v1"就新建,有就打开// 把关键文件(HTML、CSS、JS)一次性从网络"买"回来,并存入 v1 箱子
await cache.addAll([
'/', // 首页
'/style.css', // 样式表
'/app.js' // 应用脚本
]);
addAll([...])
:浏览器自动发 GET 请求,收到回应后存到箱子里self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request) // 先去所有箱子里找有没有缓存
.then(cached => {
if (cached) return cached; // 找到就直接给页面,跳过网络
return fetch(event.request) // 否则真正去"商店"买
})
);
});
fetch
钩子:所有页面的请求都会经过这里caches.match(req)
:去所有名字的箱子里试试有没有对应的回应如果你想把后来"买"的东西也顺手存起来:
self.addEventListener('fetch', event => {
event.respondWith((async () => {
const cache = await caches.open('v1');
const cached = await cache.match(event.request);
if (cached) return cached;
const res = await fetch(event.request);
// 把新买到的放进箱子,下次就能用
cache.put(event.request, res.clone());
return res;
})());
});
cache.put(req, res.clone())
:把网络回应"复制一份"放箱子里每次上线新版本最好换个名字:
// install 时用 'v2' 预缓存
// activate 时删除旧的 'v1'
这样不会混乱,也能手动清理过期数据。
CacheStorage
→ 整个储物柜,管理多个命名箱子Cache
→ 单个箱子,存「请求→回应」对open/addAll/match/put/delete
→ 打开箱子/预缓存/取缓存/动态缓存/删箱子install
钩子预缓存,在 fetch
钩子拦截并返回缓存下面分几步来拆解:
<img>/<script></link>
、XHR、fetch() 等),只要 URL 在 Service Worker 的 scope 范围内,都会触发 SW 的 fetch 事件。event.respondWith()
做了什么?self.addEventListener('fetch', event => {
event.respondWith(/* 一个 Promise,最终 resolve 一个 Response */)
})
Response
,再把它"送给页面"respondWith
,就相当于没装 SW,页面按原生规则去请求。fetch(event.request)
会不会被自己拦截?fetch()
,它向网络真正发请求,但不会触发当前 SW 的 fetch
事件(按规范:SW 自己的请求不走拦截器,防止死循环)。respondWith
里当"兜底网络请求"是安全的。fetch()
,只用 <img src="…">
之类怎么办?<img src="…">
也会触发 SW 的 fetch
事件,进而走你写的 respondWith
逻辑。fetch()
,统一在 SW 的 fetch
里处理即可。fetch
事件respondWith(Promise<Response>)
→ 拦下请求cache.match
,没命中就 fetch(event.request)
去网络Response
返回给页面,页面一律用它,不再另走网络这样,无论页面怎么触发请求(标签/XHR/fetch
),都被 SW 同一套逻辑拦截并返还你定制的缓存或网络数据。
CDN(内容分发网络)通过全球分布式节点提供内容加速和缓存服务,是前端性能优化的重要一环。
按功能演进分类
按缓存位置分类
基础配置方式
配置域名 → 设置缓存规则 → 设置回源规则 → 刷新预热
Cache-Control: max-age=86400, public
Surrogate-Control: max-age=604800 // CDN专用缓存头
Cache-Tag: product-123, category-5 // 内容标签,便于批量清除
高级配置方法
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// 自定义缓存逻辑
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
response = await fetch(request);
// 按业务需求定制缓存条件
if (response.status === 200) {
await cache.put(request, response.clone());
}
}
return response;
}
缓存管理和刷新
# 发布前预先将内容推送到CDN节点
curl -X POST "https://api.cdn.com/v1/preload" \
-d '{"urls":["http://example.com/assets/main.js"]}'
CDN缓存是现代网站性能优化的核心组成部分,合理配置可显著提升全球用户访问速度、减轻源站压力并提高容灾能力。
下面给出一套从零到上线的方案,确保你既能享受离线/缓存加速,又能安全地拿到服务端更新后的资源。
sw.js
// sw.js
const CACHE_VERSION = '20250429-v1'; // 每次部署改一下
const PRECACHE = `precache-${CACHE_VERSION}`;
const RUNTIME = `runtime-${CACHE_VERSION}`;
// 需要预缓存的"带 hash"的产物列表
const PRECACHE_URLS = [
'/',
'/static/css/main.abcd1234.css',
'/static/js/app.efgh5678.js',
//……打包后自行填充
];
// 安装:预缓存核心资源 + 立即激活
self.addEventListener('install', e => {
e.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});
// 激活:清理旧版本 + 立即接管页面
self.addEventListener('activate', e => {
e.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys.map(key => {
if (![PRECACHE, RUNTIME].includes(key)) return caches.delete(key);
})
))
.then(() => self.clients.claim())
);
});
// 拦截请求:根据策略分流
self.addEventListener('fetch', e => {
const req = e.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return; // 跨域不过滤
// 1. HTML 页面:Network-First
if (req.mode === 'navigate') {
e.respondWith(
fetch(req)
.then(res => {
caches.open(RUNTIME).then(c => c.put(req, res.clone()));
return res;
})
.catch(() => caches.match(req))
);
return;
}
// 2. 打包"带 hash"文件:Cache-First
if (PRECACHE_URLS.includes(url.pathname)) {
e.respondWith(
caches.match(req).then(cached => cached || fetch(req))
);
return;
}
// 3. 其他静态资源(CSS/JS/图片):Stale-While-Revalidate
if (req.destination === 'style' ||
req.destination === 'script' ||
req.destination === 'image') {
e.respondWith((async () => {
const cache = await caches.open(RUNTIME);
const cached = await cache.match(req);
const network = fetch(req).then(res => {
cache.put(req, res.clone());
return res;
});
return cached || network;
})());
return;
}
// 4. API → Network-First + 缓存回退
if (url.pathname.startsWith('/api/')) {
e.respondWith(
fetch(req)
.then(res => {
caches.open(RUNTIME).then(c => c.put(req, res.clone()));
return res;
})
.catch(() => caches.match(req))
);
return;
}
});
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then(reg => {
reg.onupdatefound = () => {
const newW = reg.installing;
newW.onstatechange = () => {
if (newW.state === 'installed' && navigator.serviceWorker.controller) {
// 通知用户"有新版本,点击刷新"
alert('新版本已就绪,请刷新页面');
}
};
};
})
.catch(console.error);
}
</script>
skipWaiting()
+ clients.claim()
确保新 SW 安装后立刻激活并接管旧页面。onupdatefound/onstatechange
探测到新 SW 安装完成后,可提示用户手动刷新以拿到最新内容。CACHE_VERSION
→ 发布到服务器install
钩子把带 hash 的文件预缓存CACHE_VERSION
)决定了预缓存列表和旧缓存清理时机,一定要和部署流水线挂钩。通过本文的系统梳理,我们可以看到前端缓存体系是一个多层次、多维度的技术栈,每一层都有其特定的应用场景和优势:
HTTP 缓存:作为最基础的缓存机制,通过 Cache-Control
、Expires
、ETag
等响应头实现资源复用,减少网络请求。从早期的 Expires
到现代的 Cache-Control
和协商缓存机制,展现了 Web 标准的不断演进。
Service Worker 缓存:突破了传统 HTTP 缓存的限制,将缓存控制权从服务器转移到客户端,实现了离线访问、预缓存等高级功能,为 PWA 应用提供了坚实基础。
CDN 缓存:通过全球分布式节点网络,将内容部署在离用户最近的地方,大幅降低了访问延迟,提高了全球用户体验。从简单的静态资源分发到现代的边缘计算平台,CDN 已成为前端性能优化的关键基础设施。
运行时内存缓存:从早期的 DOM 引用缓存,到现代的状态管理工具和数据获取库,前端应用内部的缓存机制也在不断演进,优化渲染性能和用户体验。
在实际应用中,这些缓存策略并非孤立存在,而是形成了一个完整的缓存链路:
用户请求 → Service Worker 拦截 → 内存缓存 → HTTP缓存 → CDN缓存 → 源服务器
每一层缓存都是为了解决特定的性能痛点:
制定合理的缓存策略时,应当考虑以下几个关键因素:
最佳实践建议:
缓存虽好,但也需警惕过度缓存带来的问题,如内容过期不更新、内存占用过大等。理想的缓存系统应当是"聪明"的——知道什么内容该缓存,缓存多久,以及何时主动失效。
随着边缘计算、AI 和 5G 技术的发展,未来的前端缓存体系将更加智能化,能够基于用户行为预测、网络状况和内容重要性自动调整缓存策略,在性能和时效性之间取得更好的平衡。
掌握前端缓存体系,不仅是提升应用性能的关键技能,也是理解 Web 架构演进的重要视角。在实际开发中,应当根据项目特点,灵活组合各种缓存策略,构建既快速又可靠的前端应用。