前端实现 SWR(Stale-While-Revalidate)缓存策略
SWR 是由 Vercel 提出的客户端缓存策略,核心是「优先返回缓存数据(Stale)→ 后台异步重新请求验证(Revalidate)→ 数据更新后刷新视图」,既保证用户体验(无加载等待),又能保证数据最终一致性。以下分「基于官方 SWR 库(推荐)」和「手动实现(理解原理)」两种方案讲解,覆盖 React 生态(最常用),也适配非 React 场景。
一、核心原理回顾
SWR 执行流程:
- 首次请求:无缓存 → 发起请求 → 数据存入缓存 → 渲染视图;
- 再次请求:有缓存 → 立即返回缓存数据渲染 → 后台静默发起新请求;
- 请求完成:
- 新数据与缓存一致 → 无操作;
- 新数据更新 → 替换缓存 → 触发视图刷新;
- 请求失败 → 保留旧缓存,不影响当前视图(可提示错误)。
二、方案 1:使用官方 SWR 库(React 生态推荐)
官方 swr 库(https://swr.vercel.app/)封装了完整的 SWR 逻辑,支持缓存、重新验证、聚焦刷新、网络恢复刷新等,开箱即用。
1. 安装依赖
# npm
npm install swr
# yarn
yarn add swr
# pnpm
pnpm add swr
2. 基础使用(核心示例)
封装请求函数 + 使用 useSWR Hook,实现「先缓存后更新」:
import React from 'react';
import useSWR from 'swr';
// 1. 封装通用请求函数(适配 Fetch/AXIOS)
const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error('请求失败');
return res.json();
};
// 2. 业务组件中使用 SWR
function UserList() {
// 核心:useSWR(缓存键, 请求函数, 配置项)
const { data, error, isLoading, mutate } = useSWR(
'/api/users', // 缓存键:通常是请求 URL(唯一标识缓存)
fetcher,
{
// 核心配置(按需调整)
revalidateOnFocus: true, // 页面聚焦时重新验证(比如切回浏览器标签)
revalidateOnReconnect: true, // 网络恢复时重新验证
dedupingInterval: 5000, // 5s 内相同请求去重(避免重复请求)
staleWhileRevalidate: 30000, // 缓存「新鲜期」30s,过期后仍可用但后台更新
revalidateIfStale: true, // 缓存过期后自动重新验证
}
);
// 加载状态(首次请求无缓存时)
if (isLoading) return <div>加载中...</div>;
// 错误处理(仅新请求失败,缓存仍保留)
if (error) return <div>请求失败:{error.message}(展示缓存数据)</div>;
return (
<div>
<h3>用户列表(SWR 缓存)</h3>
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
{/* 手动触发重新验证(比如点击刷新按钮) */}
<button onClick={() => mutate()}>手动刷新数据</button>
</div>
);
}
export default UserList;
3. 核心配置说明(控制缓存行为)
| 配置项 |
作用 |
staleWhileRevalidate |
缓存「新鲜期」(ms),过期后数据标记为「过期但可用」,后台自动更新 |
dedupingInterval |
相同缓存键的请求去重时间(ms),避免短时间重复请求 |
revalidateOnFocus |
页面聚焦时是否重新验证(比如从其他标签切回) |
revalidateOnReconnect |
网络从断开恢复时是否重新验证 |
revalidateIfStale |
缓存过期后,是否自动后台重新验证 |
refreshInterval |
定时轮询重新验证(ms),比如 5000 表示每 5s 刷新一次 |
fallbackData |
初始回退数据(无缓存、无请求结果时展示) |
persist |
持久化缓存(需配合存储适配器,如 localStorage) |
4. 持久化缓存(localStorage 保存,页面刷新不丢失)
默认 SWR 缓存仅在内存中,页面刷新后丢失,可通过 SWRConfig + 存储适配器实现持久化:
import React from 'react';
import { SWRConfig } from 'swr';
import localforage from 'localforage'; // 推荐用 localforage 处理存储(兼容多端)
// 1. 自定义存储适配器
const storage = {
get: async (key) => {
try {
return await localforage.getItem(key);
} catch (err) {
return null;
}
},
set: async (key, value) => {
await localforage.setItem(key, value);
},
remove: async (key) => {
await localforage.removeItem(key);
},
};
// 2. 全局配置 SWR
function App() {
return (
<SWRConfig
value={{
fetcher: (url) => fetch(url).then((res) => res.json()),
cache: storage, // 启用持久化缓存
staleWhileRevalidate: 60 * 1000, // 1分钟新鲜期
revalidateOnFocus: true,
}}
>
<UserList />
</SWRConfig>
);
}
5. 高级用法:依赖请求(比如先查用户 ID,再查用户详情)
SWR 支持缓存键为 null 时暂停请求,适配依赖场景:
function UserDetail({ userId }) {
// 缓存键为 null 时,不发起请求
const { data: user } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);
if (!userId) return <div>请选择用户</div>;
if (!user) return <div>加载中...</div>;
return (
<div>
<h3>用户详情:{user.name}</h3>
<p>邮箱:{user.email}</p>
</div>
);
}
三、方案 2:手动实现 SWR(无库依赖,理解核心逻辑)
若项目未使用 React 或不想引入第三方库,可手动封装 SWR 逻辑,核心是「缓存管理 + 异步重新验证」。
1. 核心设计
- 缓存存储:内存缓存(Map)+ 可选 localStorage 持久化;
- 状态管理:维护「缓存数据、加载状态、错误状态」;
- 重新验证:支持页面聚焦、定时、手动触发;
- 核心逻辑:先读缓存 → 异步请求 → 更新缓存 + 视图。
2. 完整实现(React Hook 版)
import React, { useState, useEffect, useRef } from 'react';
// 1. 全局缓存池(内存缓存)
const cache = new Map();
// 缓存元信息(记录过期时间)
const cacheMeta = new Map();
/**
* 手动实现 SWR Hook
* @param {string} key - 缓存键(请求URL)
* @param {Function} fetcher - 请求函数
* @param {Object} options - 配置项
*/
function useCustomSWR(key, fetcher, options = {}) {
const {
staleWhileRevalidate = 30000, // 30s 新鲜期
revalidateOnFocus = true,
refreshInterval = 0, // 定时刷新,0 表示关闭
} = options;
// 状态管理
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// 避免重复请求的锁
const isRevalidating = useRef(false);
// 2. 从缓存读取数据(初始化/缓存更新时)
useEffect(() => {
if (!key) return;
// 读取缓存
const cachedData = cache.get(key);
const cachedMeta = cacheMeta.get(key) || { expireTime: 0 };
if (cachedData) {
setData(cachedData);
setError(null);
setIsLoading(false);
} else {
// 无缓存,标记加载中
setIsLoading(true);
}
}, [key]);
// 3. 核心:重新验证函数(后台异步请求)
const revalidate = async () => {
if (!key || isRevalidating.current) return;
isRevalidating.current = true;
try {
// 后台发起新请求
const newData = await fetcher(key);
// 更新缓存
cache.set(key, newData);
cacheMeta.set(key, {
expireTime: Date.now() + staleWhileRevalidate,
});
// 更新视图
setData(newData);
setError(null);
} catch (err) {
setError(err);
// 失败不覆盖缓存,保留旧数据
} finally {
setIsLoading(false);
isRevalidating.current = false;
}
};
// 4. 首次请求/缓存过期时触发重新验证
useEffect(() => {
if (!key) return;
const cachedMeta = cacheMeta.get(key) || { expireTime: 0 };
// 无缓存 或 缓存过期 → 触发验证
if (!cache.has(key) || Date.now() > cachedMeta.expireTime) {
revalidate();
}
}, [key]);
// 5. 页面聚焦时重新验证
useEffect(() => {
if (!revalidateOnFocus || !key) return;
const handleFocus = () => revalidate();
window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
}, [key, revalidateOnFocus]);
// 6. 定时刷新
useEffect(() => {
if (!refreshInterval || !key) return;
const timer = setInterval(() => revalidate(), refreshInterval);
return () => clearInterval(timer);
}, [key, refreshInterval]);
// 7. 手动触发刷新的函数
const mutate = () => revalidate();
return { data, error, isLoading, mutate };
}
// ---------------------- 使用示例 ----------------------
function ProductList() {
const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error('请求失败');
return res.json();
};
const { data, error, isLoading, mutate } = useCustomSWR(
'/api/products',
fetcher,
{
staleWhileRevalidate: 60 * 1000, // 1分钟新鲜期
revalidateOnFocus: true,
refreshInterval: 10 * 1000, // 每10s刷新一次
}
);
if (isLoading && !data) return <div>加载中...</div>;
if (error) return <div>请求失败:{error.message}(展示缓存数据)</div>;
return (
<div>
<h3>商品列表(自定义 SWR)</h3>
<ul>
{data?.map((item) => (
<li key={item.id}>{item.name} - ¥{item.price}</li>
))}
</ul>
<button onClick={mutate}>手动刷新</button>
</div>
);
}
export default ProductList;
3. 非 React 场景适配(原生 JS)
若项目是原生 JS/ Vue / Angular,核心逻辑不变,只需替换状态管理方式:
// 原生 JS 版 SWR 工具函数
class SWR {
constructor(fetcher, options = {}) {
this.fetcher = fetcher;
this.options = {
staleWhileRevalidate: 30000,
revalidateOnFocus: true,
...options,
};
this.cache = new Map();
this.cacheMeta = new Map();
this.listeners = new Map(); // 监听缓存更新的回调
}
// 读取数据(核心 SWR 逻辑)
async get(key) {
// 1. 读取缓存
const cachedData = this.cache.get(key);
const cachedMeta = this.cacheMeta.get(key) || { expireTime: 0 };
const isStale = Date.now() > cachedMeta.expireTime;
// 2. 触发监听回调(返回缓存数据)
this.notify(key, {
data: cachedData,
isLoading: !cachedData,
error: null,
});
// 3. 后台重新验证(缓存不存在/过期)
if (!cachedData || isStale) {
try {
const newData = await this.fetcher(key);
this.cache.set(key, newData);
this.cacheMeta.set(key, {
expireTime: Date.now() + this.options.staleWhileRevalidate,
});
// 新数据触发回调
this.notify(key, { data: newData, isLoading: false, error: null });
} catch (err) {
this.notify(key, { data: cachedData, isLoading: false, error: err });
}
}
return cachedData;
}
// 监听缓存更新
on(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(callback);
}
// 触发监听回调
notify(key, payload) {
const callbacks = this.listeners.get(key) || [];
callbacks.forEach((cb) => cb(payload));
}
// 手动刷新
mutate(key) {
return this.get(key);
}
}
// ---------------------- 使用示例 ----------------------
// 1. 初始化 SWR 实例
const swr = new SWR(async (url) => {
const res = await fetch(url);
return res.json();
}, { staleWhileRevalidate: 60000 });
// 2. 监听数据更新并渲染
const renderProducts = (payload) => {
const container = document.getElementById('product-container');
if (payload.isLoading && !payload.data) {
container.innerHTML = '<div>加载中...</div>';
return;
}
if (payload.error) {
container.innerHTML = `<div>请求失败:${payload.error.message}</div>`;
return;
}
container.innerHTML = `
<ul>
${payload.data.map(item => `<li>${item.name} - ¥${item.price}</li>`).join('')}
</ul>
<button id="refresh">手动刷新</button>
`;
// 绑定手动刷新事件
document.getElementById('refresh').addEventListener('click', () => {
swr.mutate('/api/products');
});
};
// 3. 发起请求(先缓存后更新)
swr.on('/api/products', renderProducts);
swr.get('/api/products');
四、关键注意事项
1. 缓存键设计
- 缓存键需唯一标识请求,建议用「URL + 参数序列化」,比如:
// 带参数的请求,缓存键需包含参数
const key = `/api/users?page=${page}&size=${size}`;
// 复杂参数可序列化
const key = `/api/users?${JSON.stringify({ page, size })}`;
2. 错误处理原则
- 重新验证失败时,绝对不覆盖旧缓存,避免用户看到空白/错误页面;
- 可增加「错误重试」逻辑,比如失败后 3s 自动重试:
const revalidate = async () => {
if (!key || isRevalidating.current) return;
isRevalidating.current = true;
let retryCount = 0;
while (retryCount < 3) { // 最多重试3次
try {
const newData = await fetcher(key);
cache.set(key, newData);
setData(newData);
setError(null);
break; // 成功则退出重试
} catch (err) {
retryCount++;
if (retryCount >= 3) setError(err);
await new Promise(resolve => setTimeout(resolve, 3000)); // 3s 重试
}
}
isRevalidating.current = false;
};
3. 性能优化
- 请求去重:避免同一缓存键同时发起多个请求(用
isRevalidating 锁);
- 缓存清理:限制缓存池大小,避免内存溢出(比如最多缓存 100 条,超出则删除最久未使用的);
// 缓存清理逻辑(LRU 策略)
const MAX_CACHE_SIZE = 100;
if (cache.size > MAX_CACHE_SIZE) {
const oldestKey = Array.from(cache.keys()).shift();
cache.delete(oldestKey);
cacheMeta.delete(oldestKey);
}
- 防抖/节流:对高频触发的重新验证(比如搜索框)加节流:
import { throttle } from 'lodash';
const revalidateThrottled = throttle(revalidate, 1000); // 1s 内仅触发一次
4. 适用场景
- ✅ 高频访问的静态/准静态数据(如商品列表、用户信息);
- ✅ 对实时性要求不高,但要求体验流畅的场景;
- ❌ 强实时性场景(如秒杀倒计时、聊天消息)→ 建议用 WebSocket;
- ❌ 一次性请求(如表单提交)→ 无需缓存。
五、总结
- 推荐方案:React 项目直接用官方
swr 库,开箱即用,支持丰富的配置和生态;
- 手动实现:非 React 项目或需定制化场景,核心是「缓存池 + 异步重新验证 + 状态通知」;
- 核心原则:缓存优先,后台更新,失败不兜底,既保证体验又保证数据一致性。
通过以上实现,前端可完整落地 SWR 策略,达到「先展示缓存,后台静默更新」的核心效果。
前端实现 SWR(Stale-While-Revalidate)缓存策略
SWR 是由 Vercel 提出的客户端缓存策略,核心是「优先返回缓存数据(Stale)→ 后台异步重新请求验证(Revalidate)→ 数据更新后刷新视图」,既保证用户体验(无加载等待),又能保证数据最终一致性。以下分「基于官方 SWR 库(推荐)」和「手动实现(理解原理)」两种方案讲解,覆盖 React 生态(最常用),也适配非 React 场景。
一、核心原理回顾
SWR 执行流程:
二、方案 1:使用官方 SWR 库(React 生态推荐)
官方
swr库(https://swr.vercel.app/)封装了完整的 SWR 逻辑,支持缓存、重新验证、聚焦刷新、网络恢复刷新等,开箱即用。1. 安装依赖
2. 基础使用(核心示例)
封装请求函数 + 使用
useSWRHook,实现「先缓存后更新」:3. 核心配置说明(控制缓存行为)
staleWhileRevalidatededupingIntervalrevalidateOnFocusrevalidateOnReconnectrevalidateIfStalerefreshInterval5000表示每 5s 刷新一次fallbackDatapersist4. 持久化缓存(localStorage 保存,页面刷新不丢失)
默认 SWR 缓存仅在内存中,页面刷新后丢失,可通过
SWRConfig+ 存储适配器实现持久化:5. 高级用法:依赖请求(比如先查用户 ID,再查用户详情)
SWR 支持缓存键为
null时暂停请求,适配依赖场景:三、方案 2:手动实现 SWR(无库依赖,理解核心逻辑)
若项目未使用 React 或不想引入第三方库,可手动封装 SWR 逻辑,核心是「缓存管理 + 异步重新验证」。
1. 核心设计
2. 完整实现(React Hook 版)
3. 非 React 场景适配(原生 JS)
若项目是原生 JS/ Vue / Angular,核心逻辑不变,只需替换状态管理方式:
四、关键注意事项
1. 缓存键设计
2. 错误处理原则
3. 性能优化
isRevalidating锁);4. 适用场景
五、总结
swr库,开箱即用,支持丰富的配置和生态;通过以上实现,前端可完整落地 SWR 策略,达到「先展示缓存,后台静默更新」的核心效果。