Next.js中如何缓存每日仅更新一次的数据集?
问题背景
用Next.js开发NBA赛事网站,PostgreSQL数据库存储约7万条赛事记录,通过tRPC的getGames接口全量获取数据渲染热力图,每次加载耗时约10秒。数据仅在每日赛事结束后更新一次,需要实现1天的缓存来减少加载时间和数据库请求。尝试了React Query的staleTime/gcTime以及tRPC文档中的请求头配置,未达到预期效果。
当前核心代码
tRPC API接口
getGames: publicProcedure.query(async ({ ctx }) => { const allGames = await ctx.db.query.nbaGames.findMany(); return { games: allGames, }; }),
组件数据获取
const { data: gamesData, isLoading: loadingGamesData } = api.game.getGames.useQuery();
优化方案
1. 客户端:React Query 精准缓存配置
全局默认配置可能被单个查询的配置覆盖,建议直接在useQuery中单独设置缓存参数,确保优先级更高:
const { data: gamesData, isLoading: loadingGamesData } = api.game.getGames.useQuery({ staleTime: 24 * 60 * 60 * 1000, // 1天 gcTime: 24 * 60 * 60 * 1000, // 1天 cacheTime: 24 * 60 * 60 * 1000, // 兼容旧版React Query });
若不需要全局默认缓存规则,可移除TRPC Provider中queryClient的全局staleTime/gcTime配置,避免干扰单个查询的设置。
2. 服务端:tRPC接口+数据库查询缓存
2.1 给tRPC接口设置响应缓存头
在tRPC的procedure层面直接设置响应头,比客户端传请求头更可靠:
import { publicProcedure } from '../trpc'; const ONE_DAY = 24 * 60 * 60; getGames: publicProcedure .middleware(async ({ ctx, next }) => { // 设置响应缓存头 ctx.res?.setHeader('Cache-Control', `s-maxage=${ONE_DAY}, stale-while-revalidate=${ONE_DAY}`); return next(); }) .query(async ({ ctx }) => { const allGames = await ctx.db.query.nbaGames.findMany(); return { games: allGames }; }),
2.2 缓存数据库查询结果
直接缓存7万条数据的查询结果,避免每次请求都扫描全表(以Redis为例):
import Redis from 'ioredis'; const redis = new Redis(process.env.REDIS_URL); getGames: publicProcedure.query(async ({ ctx }) => { const cacheKey = 'nba:all-games'; // 先查缓存 const cachedGames = await redis.get(cacheKey); if (cachedGames) { return { games: JSON.parse(cachedGames) }; } // 缓存不存在则查数据库 const allGames = await ctx.db.query.nbaGames.findMany(); // 存入缓存,过期时间1天 await redis.set(cacheKey, JSON.stringify(allGames), 'EX', 24 * 60 * 60); return { games: allGames }; }),
每日赛事结束后,手动删除Redis中的nba:all-games缓存键,触发重新查询。
3. Next.js 增量静态再生(ISR)
利用Next.js的ISR预渲染页面,直接生成包含热力图数据的静态页面,用户访问时无需请求API或数据库:
3.1 页面组件中预获取数据
在页面的getStaticProps中获取数据,并设置revalidate为1天:
import { api } from '../utils/api'; import { createContext } from '../server/context'; export async function getStaticProps() { const ctx = await createContext(); const { games } = await api.game.getGames.query(undefined, ctx); return { props: { games }, revalidate: 24 * 60 * 60, // 1天后自动重新生成页面 }; } export default function GamesHeatmapPage({ games }) { // 直接用props中的games渲染热力图,无需再调用useQuery return <Heatmap data={games} />; }
3.2 手动触发重新生成
如果需要在每日赛事结束后立即更新,可写一个API路由触发重新生成:
// pages/api/revalidate.ts export default async function handler(req, res) { if (req.query.secret !== process.env.REVALIDATE_SECRET) { return res.status(401).json({ message: 'Invalid secret' }); } try { await res.revalidate('/'); // 替换为你的热力图页面路径 return res.json({ revalidated: true }); } catch (err) { return res.status(500).send('Error revalidating'); } }
每日赛事结束后调用这个API,即可立即更新页面数据。
优先级建议
优先采用Next.js ISR方案,能最大程度减少客户端加载时间和数据库请求;其次结合Redis缓存数据库查询结果,进一步降低服务端压力;最后确保客户端React Query配置正确,提升用户二次访问的体验。
内容的提问来源于stack exchange,提问作者hakc333

