如何缓存Next.js中getStaticProps的服务端请求?
缓存Next.js服务端Fetch请求(按Locale维度去重)
问题背景
当前项目中每个页面的getStaticProps都会发起相同的Fetch请求,累计达数千次,请求仅以locale作为唯一参数。同时存在以下限制:
- Next.js的Fetch polyfill会忽略
Cache-Policy请求头,无法依赖平台缓存机制 - React-Query体积过大,仅客户端筛选产品时需用,服务端场景无需引入
- 当前通过硬编码Account Managers的JSON控制包体积,但维护成本高,需更灵活的缓存方案
解决方案
方案1:内存缓存(构建/服务器进程内复用)
通过全局Map实现内存缓存,同一进程内同Locale的请求仅发起一次,同时处理并发请求的竞态问题,避免重复请求。
// 全局内存缓存,键为locale,值为AccountManager或Promise<AccountManager> const accountManagerCache = new Map<string, Promise<AccountManager> | AccountManager>(); export async function getAccountManagerByLocale( tld = 'com' ): Promise<AccountManager> { // 检查缓存是否存在 if (accountManagerCache.has(tld)) { const cached = accountManagerCache.get(tld); // 处理正在进行的请求Promise,避免重复发起 if (cached instanceof Promise) { return cached; } return cached; } // 发起请求并将Promise存入缓存 const fetchPromise = fetch( `${process.env.NEXT_PUBLIC_CRM_API_URL}/account-manager/?locale=${tld}`, { headers: { accept: 'application/json, text/plain, */*', 'accept-language': 'en', }, } ) .then(res => res.json()) .then(accountManagersList => accountManagersList[0]) .catch(err => { // 请求失败时移除缓存,避免缓存错误结果 accountManagerCache.delete(tld); throw err; }); accountManagerCache.set(tld, fetchPromise); return fetchPromise; }
优势:实现简单,性能极高,构建过程中同Locale请求完全复用,无额外IO开销。
限制:缓存仅存在于当前进程内存中,进程重启或多进程部署时缓存会失效,适合单进程服务器或一次性构建场景。
方案2:文件系统缓存(持久化跨进程复用)
通过本地文件存储缓存数据,配合过期时间检查,实现跨进程、跨重启的缓存复用,同时和getStaticProps的revalidate时间保持一致。
import fs from 'fs/promises'; import path from 'path'; // 缓存存储目录,需确保项目根目录下存在该路径 const CACHE_DIR = path.join(process.cwd(), '.cache', 'account-managers'); // 确保缓存目录存在 async function ensureCacheDir() { await fs.mkdir(CACHE_DIR, { recursive: true }); } export async function getAccountManagerByLocale( tld = 'com' ): Promise<AccountManager> { const cacheFilePath = path.join(CACHE_DIR, `${tld}.json`); // 检查缓存文件是否存在且未过期 try { const cachedContent = await fs.readFile(cacheFilePath, 'utf-8'); const cachedData = JSON.parse(cachedContent); // 获取缓存文件的修改时间,判断是否过期(和revalidate时间一致为120秒) const fileStats = await fs.stat(cacheFilePath); const cacheAge = Date.now() - fileStats.mtime.getTime(); if (cacheAge < 120 * 1000) { return cachedData; } } catch (err) { // 文件不存在/读取失败,跳过缓存直接请求 } // 发起请求获取最新数据 const res = await fetch( `${process.env.NEXT_PUBLIC_CRM_API_URL}/account-manager/?locale=${tld}`, { headers: { accept: 'application/json, text/plain, */*', 'accept-language': 'en', }, } ); const accountManagersList = await res.json(); const targetData = accountManagersList[0]; // 将最新数据写入缓存文件 await ensureCacheDir(); await fs.writeFile(cacheFilePath, JSON.stringify(targetData)); return targetData; }
优势:缓存持久化,跨进程/重启依然有效,适合多进程部署或需要长期复用缓存的场景。
限制:存在文件IO开销,性能略低于内存缓存,需维护缓存目录的权限和清理策略。
注意事项
- 两种方案均在服务端运行,不会增加客户端包体积,符合项目控制包体积的需求
- 内存缓存中需处理请求失败的情况,避免缓存错误结果导致后续请求失败
- 文件系统缓存的过期时间建议和
getStaticProps的revalidate值保持一致,确保数据更新同步
内容的提问来源于stack exchange,提问作者Cauan Goes
相关产品推荐
相关产品推荐

