我们的首页推荐商品列表中,数据的实时性直接影响用户购买决策和平台转化率,所以我们使用 SWR(Stale-While-Revalidate) 缓存策略,先展示缓存数据,后台更新,减少白屏时间。
不过,我们遇到了一个典型问题:商品列表数据陈旧,更新机制失效,导致用户看到的推荐商品与实际库存或促销活动不同步。
本文详细分析这一问题的排查过程、修复方案,并通过真实案例复盘开发中的“踩坑错题”。文章将覆盖以下内容:
在我们的线上商城项目中,使用了 SWR 进行数据缓存和获取首页推荐商品列表。初始实现看起来工作正常,但用户反馈有时会看到过期的商品信息,比如已经下架的商品仍然显示在推荐列表中,或者新上架的商品长时间不出现。经过初步分析,这明显是缓存策略导致的数据不一致问题。
data
长时间未更新。可能的原因包括:
revalidate
逻辑未触发。dedupingInterval
)设置过长。Cache-Control
)。首先,我们需要深入理解 SWR 的工作机制。SWR 采用 stale-while-revalidate 策略,这意味着它会先返回缓存的数据(stale),然后在后台重新验证数据(revalidate)。
SWR 工作流程示意
查看SWR的使用代码:
import useSWR from 'swr';
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);
const HomeProductList = () => {
// SWR配置
const { data: products, error } = useSWR(
'/api/home/products', // 缓存key
fetcher,
{
revalidateOnMount: false, // 挂载时是否重新验证
revalidateOnFocus: false, // 页面聚焦时是否重新验证
revalidateOnReconnect: false, // 网络重连时是否重新验证
dedupingInterval: 30 * 60 * 1000, // 去重间隔(30分钟)
ttl: 30 * 60 * 1000, // 缓存过期时间(30分钟)
}
);
if (error) return <div>加载失败</div>;
if (!products) return <div>加载中...</div>;
return (
<div className="product-list">
{products.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
};
export default HomeProductList;
配置项问题分析:
revalidateOnFocus: false
:用户切换标签页或从后台返回时,SWR不会重新验证数据;revalidateOnReconnect: false
:网络从断开恢复时,不触发重新验证;revalidateOnMount: false
:组件挂载时不重新验证,直接使用缓存(若存在);dedupingInterval: 30分钟
:30分钟内相同key的请求会被合并,不发送新请求;ttl: 30分钟
:缓存数据30分钟内视为"新鲜",不会主动过期。疑点:配置中多个"关闭重新验证"的参数叠加,可能导致SWR长期依赖缓存,不主动获取新数据。
为了更好地理解问题,我们在关键位置添加了调试日志:
import useSWR from 'swr';
const fetcher = async (url) => {
console.log(`[DEBUG] 发起请求: ${url}`);
const response = await fetch(url);
const data = await response.json();
console.log(`[DEBUG] 请求完成: ${url}`, data);
return data;
};
function RecommendedProducts() {
const { data, error, isValidating } = useSWR('/api/recommended-products', fetcher, {
onSuccess: (data, key) => {
console.log(`[DEBUG] SWR 成功获取数据: ${key}`, data);
},
onError: (error, key) => {
console.log(`[DEBUG] SWR 获取数据失败: ${key}`, error);
}
});
console.log(`[DEBUG] 组件渲染 - data:`, data, `isValidating:`, isValidating);
if (error) {
console.log(`[DEBUG] 渲染错误状态`);
return <div>加载失败</div>;
}
if (!data) {
console.log(`[DEBUG] 渲染加载状态`);
return <div>加载中...</div>;
}
console.log(`[DEBUG] 渲染数据`, data);
return (
<div className="product-list">
{data.products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
在电商场景中,部分数据需要"实时性优先"(如秒杀商品库存),需通过SWR的mutate
API主动更新缓存。检查项目中是否实现了相关机制:
// 全局搜索发现,项目中未实现任何调用SWR mutate的代码
// 即:当后端数据更新(如新品上架)时,前端无主动触发缓存更新的逻辑
结论:缺乏主动更新机制,导致后端数据变化后,前端无法实时感知并更新缓存。
经过四层排查,确定故障根因为"缓存策略与业务实时性需求不匹配",具体表现为:
revalidateOnFocus
/revalidateOnReconnect
关闭,失去页面聚焦、网络恢复等关键触发时机;dedupingInterval
和ttl
均设为30分钟,远超电商首页数据合理的新鲜度周期(业务要求≤5分钟);针对上述根因,我们设计了一套"动态配置+主动更新+智能验证"的综合优化方案,分四步实现缓存策略与业务需求的对齐。
设计思路:根据电商首页"高频访问、中等实时性"的特点,调整SWR核心参数,平衡性能与数据新鲜度。
关键配置调整:
// ... 原有代码 ...
const HomeProductList = () => {
// 获取用户信息(从全局状态)
const { userInfo } = useContext(UserContext);
// 获取地域信息(从IP定位工具)
const { regionCode } = useRegion();
// 动态生成缓存key:包含用户ID、地域编码、当前小时(按小时更新推荐策略)
const cacheKey = `/api/home/products?userId=${userInfo.id}®ion=${regionCode}&hour=${new Date().getHours()}`;
// 优化后的SWR配置
const { data: products, error, mutate } = useSWR(
cacheKey, // 动态key
fetcher,
{
revalidateOnMount: true, // 组件挂载时强制重新验证(首次加载后,后续挂载仍验证)
revalidateOnFocus: true, // 页面聚焦时重新验证(如用户切回浏览器标签)
revalidateOnReconnect: true, // 网络重连时重新验证
dedupingInterval: 5 * 60 * 1000, // 5分钟内合并重复请求(避免抖动)
ttl: 5 * 60 * 1000, // 缓存5分钟后标记为"陈旧",下次访问触发重新验证
revalidateIfStale: true, // 若缓存陈旧,返回缓存的同时发送验证请求
errorRetryCount: 3, // 错误重试3次
errorRetryInterval: 1000, // 重试间隔1秒
}
);
// ... 渲染代码 ...
};
参数解析:
revalidateOnMount: true
:确保每次组件挂载(如用户返回首页)都触发验证,避免依赖旧缓存;revalidateOnFocus: true
:用户从其他App切回浏览器时,自动更新数据(电商用户高频切换场景适配);cacheKey动态化
:包含用户ID(区分会员/非会员)、地域编码(区分区域库存)、当前小时(匹配后端按小时更新的推荐算法),避免缓存污染;ttl: 5分钟
:根据业务需求(首页商品数据更新频率约5分钟/次)设置合理的缓存生命周期。设计思路:通过WebSocket监听后端数据变更事件(如新品上架、库存更新),实时调用SWR的mutate
API更新缓存,解决"后端变了前端不知道"的问题。
主动更新机制架构:
WebSocket客户端封装:
export const createProductWebSocket = (onMessage) => {
if (typeof window === 'undefined') return null; // 服务端渲染兼容
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${wsProtocol}//${window.location.host}/ws/product-updates`);
ws.onopen = () => {
console.log('商品更新WebSocket连接成功');
// 连接成功后发送用户信息,便于后端推送个性化更新
ws.send(JSON.stringify({
type: 'subscribe',
userId: userInfo.id,
region: regionCode
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
onMessage(data); // 外部传入消息处理函数
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
ws.onclose = () => {
console.log('WebSocket连接关闭,3秒后重连');
setTimeout(() => createProductWebSocket(onMessage), 3000); // 断线重连
};
return ws;
};
在商品列表组件中集成WebSocket,实现主动更新:
const HomeProductList = () => {
// ... 原有SWR配置代码 ...
// 初始化WebSocket,监听商品更新事件
useEffect(() => {
const ws = createProductWebSocket((message) => {
if (message.type === 'product_update') {
console.log('收到商品更新通知,触发缓存更新:', message.productId);
// 调用SWR mutate更新缓存(第二个参数为undefined,会触发重新请求)
mutate(cacheKey);
}
});
return () => {
ws?.close(); // 组件卸载时关闭连接
};
}, [cacheKey, mutate]);
// ... 渲染代码 ...
};
重点逻辑:
product_update
事件;mutate(cacheKey)
触发SWR重新请求并更新缓存;为便于后续调试,添加SWR缓存状态监控面板,实时展示缓存key、创建时间、新鲜度等信息:
import { useSWRConfig } from 'swr';
const SWRCacheMonitor = () => {
const { cache } = useSWRConfig();
const [cacheState, setCacheState] = useState({});
// 每3秒刷新一次缓存状态
useEffect(() => {
const interval = setInterval(() => {
const entries = {};
cache.forEach((value, key) => {
entries[key] = {
isStale: Date.now() - value.data.timestamp > value.config.ttl, // 判断是否过期
timestamp: new Date(value.data.timestamp).toLocaleString(),
ttl: value.config.ttl / 1000 + 's'
};
});
setCacheState(entries);
}, 3000);
return () => clearInterval(interval);
}, [cache]);
return (
<div className="cache-monitor">
<h3>SWR缓存监控</h3>
<pre>{JSON.stringify(cacheState, null, 2)}</pre>
</div>
);
};
// 在开发环境挂载到首页
{process.env.NODE_ENV === 'development' && <SWRCacheMonitor />}
效果:开发环境下可实时查看缓存状态,快速定位"缓存未更新""key重复"等问题。
不同类型的商品数据对实时性要求不同(如普通商品vs秒杀商品),需细化缓存策略:
// 封装差异化SWR hooks
export const useProductSWR = (productType) => {
// 根据商品类型返回不同配置
const getSWRConfig = () => {
switch (productType) {
case 'seckill': // 秒杀商品:实时性优先
return {
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 1000, // 1秒去重
ttl: 1000, // 1秒过期
revalidateOnMount: true
};
case 'recommend': // 推荐商品:平衡性能与实时性
return {
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 30000, // 30秒去重
ttl: 300000, // 5分钟过期
revalidateOnMount: true
};
default: // 默认配置
return {
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 60000, // 1分钟去重
ttl: 600000, // 10分钟过期
};
}
};
// 动态key(包含商品类型)
const cacheKey = `/api/home/products?type=${productType}&userId=${userInfo.id}®ion=${regionCode}&hour=${new Date().getHours()}`;
return useSWR(cacheKey, fetcher, getSWRConfig());
};
// 使用方式
const { data: seckillProducts } = useProductSWR('seckill');
const { data: recommendProducts } = useProductSWR('recommend');
架构解析:通过自定义hooks封装差异化策略,实现"同一页面不同数据类型,不同缓存规则",满足精细化业务需求。
// 优化前:缓存未更新日志
[10:05:23] SWR: cache hit for key "/api/home/products"
[10:05:23] SWR: returning cached data (timestamp: 09:35:10)
[10:05:23] SWR: dedupingInterval not expired, skip request
// 优化后:重新验证成功日志
[14:20:15] SWR: cache hit for key "/api/home/products?type=recommend&userId=123®ion=SH&hour=14"
[14:20:15] SWR: returning cached data (timestamp: 14:19:50)
[14:20:15] SWR: revalidateOnMount enabled, send revalidation request
[14:20:16] SWR: request success, update cache (new timestamp: 14:20:16)
[14:20:16] SWR: trigger re-render with new data
// WebSocket主动更新日志
[15:30:00] WebSocket: received product_update event (productId: 10086)
[15:30:00] SWR: mutate key "/api/home/products?type=recommend&userId=123®ion=SH&hour=15"
[15:30:01] SWR: revalidation success, cache updated
JSON.stringify({...})
生成复杂key。dedupingInterval
和ttl
;SWRConfig
Provider创建多实例,区分全局缓存(如用户信息)和局部缓存(如页面数据)。通过调整 SWR 配置参数、建立 WebSocket 实时通信、监听页面可见性变化以及提供手动刷新功能,我们成功解决了数据陈旧问题,显著提升了用户体验。这一实践不仅解决了当前问题,更为类似场景提供了可复用的优化思路。
通过本文,你可以收获:
ttl
/dedupingInterval
/mutate
)的实际影响与配置原则;缓存策略没有一劳永逸的解决方案,只有最适合业务特性的设计。希望通过本文的分享,能够帮助读者在前端开发中更好地使用SWR缓存机制,提升应用性能和用户体验。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。