You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何缓存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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 02:51:03