Published on

Next.js 数据缓存机制全面解析:从 fetch 到 use cache

在现代 Web 开发中,缓存策略对于应用性能和用户体验至关重要。Next.js 作为一个流行的 React 框架,提供了多种数据缓存机制,帮助开发者优化数据获取和渲染过程。本文将深入探讨 Next.js 中的四种主要缓存机制:扩展的 fetch API、use cache 指令、unstable_cache 函数以及 React 的 cache 函数,并提供实用的选择指南。

缓存机制概览

Next.js 提供的缓存机制各有特点,它们在语法、用途、持久性和适用场景上存在差异。在深入每种机制之前,让我们先对它们有个整体认识:

  • 扩展的 fetch API:Next.js 扩展了标准的 Web fetch API,允许通过参数控制缓存行为
  • unstable_cache 函数:专为缓存数据获取函数设计的高阶函数
  • use cache 指令:实验性的指令式缓存方案,未来可能替代 unstable_cache
  • React 的 cache 函数:React 服务器组件提供的请求级缓存机制

扩展的 fetch API

Next.js 扩展了 Web 标准的 fetch API,赋予它持久化缓存能力。这是 Next.js 中最直接、最常用的数据缓存方式。

基本语法

// 基本用法
fetch(url, { 
  cache: 'force-cache' | 'no-store',
  next: { 
    revalidate: number | false,
    tags: ['tag1', 'tag2']
  }
})

缓存策略选项

  1. cache 选项

    • 'auto no cache'(默认):在开发环境中每次请求都从服务器获取,在构建时只获取一次。如果检测到路由使用了动态 API,则每次请求都会从服务器获取
    • 'force-cache':优先从缓存获取,缓存不存在或过期时从服务器获取
    • 'no-store':每次请求都从服务器获取新数据,即使路由没有使用动态 API
  2. next.revalidate 选项

    • 设置缓存生命周期(秒)
    • false:无限期缓存
    • 0:禁止缓存
    • number:指定缓存时间(秒)
  3. next.tags 选项

    • 为缓存资源添加标签
    • 可通过 revalidateTag API 按需重新验证

示例

// 默认行为 - 开发环境每次请求获取新数据,生产环境在构建时获取
const response = await fetch('https://api.example.com/data');

// 强制使用缓存
const cachedData = await fetch('https://api.example.com/data', { 
  cache: 'force-cache' 
});

// 禁用缓存,每次请求都获取新数据
const freshData = await fetch('https://api.example.com/data', { 
  cache: 'no-store' 
});

// 设置缓存时间为1小时
const revalidatedData = await fetch('https://api.example.com/data', { 
  next: { revalidate: 3600 } 
});

// 使用缓存标签,方便按需重新验证
const taggedData = await fetch('https://api.example.com/data', { 
  next: { tags: ['product-data'] } 
});

优缺点

优点

  • 语法简单,与标准 fetch API 一致
  • 与 Next.js 数据缓存系统深度集成
  • 提供精细的缓存控制选项
  • 稳定性高,不是实验性功能

缺点

  • 仅适用于 HTTP 请求
  • 不能缓存计算结果或组合多个请求的结果

unstable_cache 函数

unstable_cache 是 Next.js 提供的一个高阶函数,用于包装和缓存任意异步函数的结果,特别适合数据获取函数。

基本语法

import { unstable_cache } from 'next/cache';

const getCachedData = unstable_cache(
  async (...args) => {
    // 异步数据获取或计算逻辑
    return result;
  },
  ['cache-key-prefix'],
  { revalidate: 60, tags: ['data-tag'] }
);

参数说明

  1. 第一个参数:要缓存的异步函数
  2. 第二个参数:缓存键前缀数组
  3. 第三个参数:缓存配置选项
    • revalidate:缓存时间(秒)
    • tags:缓存标签,用于按需重新验证

示例

import { unstable_cache } from 'next/cache';

// 基本用法
const getUser = unstable_cache(
  async (userId) => {
    const res = await fetch(`https://api.example.com/users/${userId}`);
    return res.json();
  },
  ['user-data'],
  { revalidate: 3600, tags: ['user'] }
);

// 在组件中使用
export default async function UserProfile({ userId }) {
  const user = await getUser(userId);
  return <div>{user.name}</div>;
}

优缺点

优点

  • 可以缓存任意异步函数,不限于 fetch
  • 支持复杂的缓存键生成
  • 与 Next.js 的重新验证系统集成
  • 支持跨请求持久缓存

缺点

  • 名称中包含 "unstable",暗示 API 可能变更
  • 语法较为冗长
  • 主要设计用于缓存 JSON 数据

use cache 指令

use cache 是 Next.js 的一个实验性指令,用于缓存函数、组件或路由的输出。它使用了类似 'use client' 的指令式语法。

基本语法

async function getData() {
  'use cache'
  // 数据获取或计算逻辑
  return result;
}

配置选项

虽然 use cache 本身不接受参数,但可以与 Next.js 的其他 API 配合使用:

  • cacheLife API 控制缓存生命周期
  • cacheTag API 添加缓存标签

示例

// 基本用法
export async function getUser(userId) {
  'use cache'
  const res = await fetch(`https://api.example.com/users/${userId}`);
  return res.json();
}

// 在组件中使用
export default async function UserProfile({ userId }) {
  const user = await getUser(userId);
  return <div>{user.name}</div>;
}

优缺点

优点

  • 语法简洁明了
  • 可以缓存任何可序列化数据
  • 是 Next.js 缓存 API 的未来发展方向
  • 功能全面,可用于组件、路由和数据

缺点

  • 实验性功能,API 可能会变化
  • 配置选项需要使用额外的 API
  • 文档相对较少

React 的 cache 函数

React 服务器组件提供了 cache 函数,用于在单个请求的生命周期内缓存函数结果。

基本语法

import { cache } from 'react';

const getData = cache(async (...args) => {
  // 数据获取或计算逻辑
  return result;
});

示例

import { cache } from 'react';

// 缓存数据获取函数
const getUser = cache(async (userId) => {
  const res = await fetch(`https://api.example.com/users/${userId}`);
  return res.json();
});

// 在多个组件中使用
function UserProfile({ userId }) {
  const user = getUser(userId);
  return <div>{user.name}</div>;
}

function UserStats({ userId }) {
  const user = getUser(userId);  // 相同参数调用会复用缓存结果
  return <div>Posts: {user.posts.length}</div>;
}

优缺点

优点

  • 轻量级,无需额外配置
  • 适用于任何异步函数
  • 与 React 服务器组件无缝集成
  • 可在组件树的不同部分共享数据

缺点

  • 缓存仅在单个请求内有效,不是持久缓存
  • 没有过期或重新验证机制
  • 不能手动清除缓存

四种缓存机制对比

为了更清晰地理解这四种缓存机制的异同,下面是一个详细的对比表:

特性扩展的 fetchunstable_cacheuse cacheReact cache
框架Next.jsNext.jsNext.jsReact
语法函数参数函数包装指令函数包装
缓存范围HTTP 请求函数结果函数/组件/路由函数结果
缓存持久性跨请求持久跨请求持久跨请求持久仅当前请求
缓存控制cache, revalidate, tagsrevalidate, tagscacheLife, cacheTag无直接控制
数据类型HTTP 响应主要是 JSON任何可序列化数据任何返回值
稳定性稳定不稳定实验性稳定
适用场景外部 API 请求数据获取缓存全面缓存需求请求内共享数据

实际应用场景

场景一:产品列表页面

假设我们需要构建一个产品列表页面,该页面需要从 API 获取产品数据,并且数据每小时更新一次。

// 使用扩展的 fetch
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }
  }).then(res => res.json());
  
  return (
    <div>
      <h1>产品列表</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

场景二:用户个人资料页面

假设我们需要构建一个用户个人资料页面,该页面需要从多个 API 获取用户数据,并且需要进行一些数据处理。

// 使用 unstable_cache
import { unstable_cache } from 'next/cache';

const getUserProfile = unstable_cache(
  async (userId) => {
    // 获取基本信息
    const userRes = await fetch(`https://api.example.com/users/${userId}`);
    const user = await userRes.json();
    
    // 获取订单历史
    const ordersRes = await fetch(`https://api.example.com/users/${userId}/orders`);
    const orders = await ordersRes.json();
    
    // 组合和处理数据
    return {
      ...user,
      orders,
      totalSpent: orders.reduce((sum, order) => sum + order.amount, 0)
    };
  },
  ['user-profile'],
  { revalidate: 300, tags: ['user'] }
);

export default async function ProfilePage({ userId }) {
  const profile = await getUserProfile(userId);
  
  return (
    <div>
      <h1>{profile.name}的个人资料</h1>
      <p>邮箱: {profile.email}</p>
      <p>总消费: ¥{profile.totalSpent}</p>
      {/* 其他内容 */}
    </div>
  );
}

场景三:动态仪表板

假设我们需要构建一个仪表板,该仪表板需要显示多种数据,并且某些数据需要实时更新,而其他数据可以缓存。

// 混合使用多种缓存策略
import { cache } from 'react';
import { unstable_cache } from 'next/cache';

// 使用 React cache 缓存单个请求内的计算结果
const calculateMetrics = cache(async (data) => {
  // 复杂计算
  return {
    averageSales: data.reduce((sum, item) => sum + item.sales, 0) / data.length,
    topProducts: data.sort((a, b) => b.sales - a.sales).slice(0, 5)
  };
});

// 使用 unstable_cache 缓存历史数据
const getHistoricalData = unstable_cache(
  async (period) => {
    const res = await fetch(`https://api.example.com/sales/history?period=${period}`);
    return res.json();
  },
  ['sales-history'],
  { revalidate: 86400 } // 一天缓存
);

// 使用扩展的 fetch 获取实时数据
async function getLiveData() {
  const res = await fetch('https://api.example.com/sales/live', {
    cache: 'no-store' // 不缓存实时数据
  });
  return res.json();
}

// 使用 use cache 缓存整个组件渲染
async function SalesOverview({ period }) {
  'use cache'
  
  const historicalData = await getHistoricalData(period);
  const metrics = await calculateMetrics(historicalData);
  
  return (
    <div className="sales-overview">
      <h2>销售概览 ({period})</h2>
      <p>平均销售额: ¥{metrics.averageSales.toFixed(2)}</p>
      <h3>热门产品:</h3>
      <ul>
        {metrics.topProducts.map(product => (
          <li key={product.id}>{product.name} - ¥{product.sales}</li>
        ))}
      </ul>
    </div>
  );
}

export default async function Dashboard() {
  // 历史数据使用缓存
  const historicalData = await getHistoricalData('monthly');
  
  // 实时数据每次请求都获取最新
  const liveData = await getLiveData();
  
  return (
    <div className="dashboard">
      <h1>销售仪表板</h1>
      
      {/* 实时数据部分 */}
      <div className="live-data">
        <h2>实时销售数据</h2>
        <p>今日销售额: ¥{liveData.todaySales}</p>
        <p>活跃订单: {liveData.activeOrders}</p>
      </div>
      
      {/* 缓存的历史数据概览 */}
      <SalesOverview period="monthly" />
      <SalesOverview period="quarterly" />
    </div>
  );
}

场景四:博客平台

假设我们正在构建一个博客平台,需要处理不同缓存需求的页面:

// 使用 use cache 指令
export async function getBlogPost(slug) {
  'use cache'
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await res.json();
  
  // 处理内容,例如将 Markdown 转换为 HTML
  post.contentHtml = await markdownToHtml(post.content);
  
  return post;
}

// 使用 React cache 处理评论
import { cache } from 'react';

const getComments = cache(async (postId) => {
  const res = await fetch(`https://api.example.com/posts/${postId}/comments`);
  return res.json();
});

// 博客文章页面
export default async function BlogPost({ params }) {
  const { slug } = params;
  const post = await getBlogPost(slug);
  const comments = await getComments(post.id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
      
      <section className="comments">
        <h2>评论 ({comments.length})</h2>
        {comments.map(comment => (
          <div key={comment.id} className="comment">
            <h3>{comment.author}</h3>
            <p>{comment.content}</p>
          </div>
        ))}
      </section>
    </article>
  );
}

最佳实践与选择指南

基于对这四种缓存机制的深入理解,以下是一些最佳实践和选择指南:

1. 选择扩展的 fetch API 的场景

  • 简单的 HTTP 请求:当你只需要获取和缓存外部 API 数据时
  • 需要精细控制缓存行为:当你需要设置具体的缓存时间或使用缓存标签时
  • 追求稳定性:当你希望使用最稳定、最不可能变化的 API 时
// 最佳实践示例
async function getProducts(category) {
  // 需要持久缓存时,显式指定 force-cache
  const cachedResponse = await fetch(`https://api.example.com/products?category=${category}`, {
    cache: 'force-cache',
    next: { 
      revalidate: 3600,
      tags: [`products-${category}`]
    }
  });

  return cachedResponse;
}

// 在需要时重新验证
import { revalidateTag } from 'next/cache';

// 例如,在产品更新后调用
async function handleProductUpdate(category) {
  revalidateTag(`products-${category}`);
  return { success: true };
}

调试提示:开发环境中的 HMR 缓存

在开发环境中,你可能会遇到一个常见的困扰:即使设置了 cache: 'no-store' 或使用默认的 auto no cache,修改代码后数据仍然没有更新。这是因为 Next.js 在开发环境中引入了一个特殊的缓存机制 - serverComponentsHmrCache

为什么会有这个缓存?

这个缓存机制有两个主要目的:

  1. 优化开发体验:通过缓存 fetch 响应,减少重复的 API 调用,使热模块替换(HMR)过程更快速
  2. 降低开发成本:对于付费 API,减少不必要的重复调用,避免额外的费用支出
缓存的影响

这意味着在开发过程中:

  • 即使你显式设置了 cache: 'no-store'
  • 即使你使用了默认的 auto no cache 选项
  • 你的 fetch 请求仍可能被 HMR 缓存捕获,导致代码修改后看不到最新数据
如何获取最新数据?

要清除 HMR 缓存,你可以:

  1. 页面导航:在应用中切换到其他页面再返回
  2. 完整页面刷新:使用浏览器的刷新功能(F5 或 Ctrl+R)

这个机制仅存在于开发环境中,不会影响生产环境的缓存行为。更多详细信息可以参考 Next.js 官方文档中关于 serverComponentsHmrCache 的说明。

2. 选择 unstable_cache 的场景

  • 需要缓存多个请求的组合结果:当你的函数获取多个数据源并组合它们时
  • 需要缓存复杂计算:当你的函数不仅获取数据,还进行复杂处理时
  • 需要精细控制缓存键:当缓存键需要基于多个参数生成时
// 最佳实践示例
import { unstable_cache } from 'next/cache';

const getProductAnalytics = unstable_cache(
  async (productId, timeRange) => {
    // 获取多个数据源
    const [sales, views, inventory] = await Promise.all([
      fetch(`https://api.example.com/products/${productId}/sales?range=${timeRange}`).then(res => res.json()),
      fetch(`https://api.example.com/products/${productId}/views?range=${timeRange}`).then(res => res.json()),
      fetch(`https://api.example.com/products/${productId}/inventory`).then(res => res.json())
    ]);
    
    // 计算分析指标
    return {
      productId,
      timeRange,
      salesTotal: sales.reduce((sum, item) => sum + item.amount, 0),
      conversionRate: sales.length / views.length,
      stockLevel: inventory.current,
      isLowStock: inventory.current < inventory.threshold
    };
  },
  ['product-analytics'],
  { 
    revalidate: 3600,
    tags: ['analytics']
  }
);

3. 选择 use cache 的场景

  • 需要缓存整个组件或路由:当你想缓存渲染结果而不仅是数据时
  • 追求简洁的语法:当你希望使用最简单的缓存语法时
  • 愿意使用实验性功能:当你不介意使用可能会变化的 API 时
// 最佳实践示例
// 缓存整个组件渲染
async function ProductCard({ productId }) {
  'use cache'
  
  const product = await fetch(`https://api.example.com/products/${productId}`).then(res => res.json());
  const reviews = await fetch(`https://api.example.com/products/${productId}/reviews`).then(res => res.json());
  
  const averageRating = reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length;
  
  return (
    <div className="product-card">
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <div className="rating">
        <span>平均评分: {averageRating.toFixed(1)}/5</span>
        <span>({reviews.length} 条评论)</span>
      </div>
      <button>加入购物车</button>
    </div>
  );
}

4. 选择 React cache 的场景

  • 单个请求内共享数据:当你需要在组件树的不同部分共享相同的数据时
  • 避免重复计算:当你有计算密集型函数需要多次调用时
  • 无需跨请求持久化:当你只需要在单个请求内缓存数据时
// 最佳实践示例
import { cache } from 'react';

// 缓存昂贵的计算
const processUserData = cache(async (userId) => {
  const res = await fetch(`https://api.example.com/users/${userId}`);
  const userData = await res.json();
  
  // 假设这是一个复杂的处理过程
  return {
    ...userData,
    fullName: `${userData.firstName} ${userData.lastName}`,
    age: calculateAge(userData.birthDate),
    preferences: parsePreferences(userData.rawPreferences)
  };
});

// 在多个组件中使用相同的处理结果
function UserHeader({ userId }) {
  const user = processUserData(userId);
  return <h1>欢迎, {user.fullName}</h1>;
}

function UserPreferences({ userId }) {
  const user = processUserData(userId);  // 不会重复计算
  return (
    <div>
      <h2>用户偏好</h2>
      <ul>
        {user.preferences.map(pref => <li key={pref.id}>{pref.name}</li>)}
      </ul>
    </div>
  );
}

5. 组合使用多种缓存机制

在复杂应用中,通常需要组合使用多种缓存机制来满足不同需求:

// 组合使用示例
import { cache } from 'react';
import { unstable_cache } from 'next/cache';
import { revalidateTag } from 'next/cache';

// 使用扩展的 fetch 获取基础数据
async function getBaseProducts() {
  return fetch('https://api.example.com/products', {
    next: { tags: ['products'] }
  }).then(res => res.json());
}

// 使用 unstable_cache 缓存过滤逻辑
const getFilteredProducts = unstable_cache(
  async (category, minPrice, maxPrice) => {
    const allProducts = await getBaseProducts();
    
    return allProducts.filter(product => 
      product.category === category &&
      product.price >= minPrice &&
      product.price <= maxPrice
    );
  },
  ['filtered-products'],
  { tags: ['products'] }
);

// 使用 React cache 缓存单个请求内的计算
const calculateProductStats = cache(async (products) => {
  return {
    count: products.length,
    averagePrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
    priceRange: {
      min: Math.min(...products.map(p => p.price)),
      max: Math.max(...products.map(p => p.price))
    }
  };
});

// 使用 use cache 缓存整个产品列表组件
async function ProductList({ category, minPrice, maxPrice }) {
  'use cache'
  
  const products = await getFilteredProducts(category, minPrice, maxPrice);
  const stats = await calculateProductStats(products);
  
  return (
    <div>
      <h1>{category} 产品列表</h1>
      <div className="stats">
        <p>{stats.count} 件产品</p>
        <p>平均价格: ¥{stats.averagePrice.toFixed(2)}</p>
        <p>价格区间: ¥{stats.priceRange.min} - ¥{stats.priceRange.max}</p>
      </div>
      
      <ul className="products">
        {products.map(product => (
          <li key={product.id} className="product-item">
            <h3>{product.name}</h3>
            <p>¥{product.price}</p>
            <button>添加到购物车</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

// 管理界面中的产品更新函数
async function updateProduct(productData) {
  // 更新产品
  const response = await fetch(`https://api.example.com/products/${productData.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(productData)
  });
  
  // 更新成功后重新验证缓存
  if (response.ok) {
    revalidateTag('products');
  }
  
  return response.json();
}

这个示例展示了如何组合使用不同的缓存机制来构建一个高性能的产品列表页面:

  • 使用扩展的 fetch API 获取和缓存基础产品数据
  • 使用 unstable_cache 缓存过滤和排序逻辑
  • 使用 React 的 cache 函数缓存单个请求内的统计计算
  • 使用 use cache 指令缓存整个产品列表组件的渲染结果
  • 使用 revalidateTag 在产品更新后重新验证相关缓存

未来展望

随着 Next.js 和 React 的不断发展,缓存机制也在持续演进。以下是一些值得关注的趋势和可能的未来发展:

1. use cache 指令的稳定化

目前,use cache 指令仍处于实验阶段,但它代表了 Next.js 缓存 API 的发展方向。随着时间推移,我们可以期待:

  • use cache 指令的稳定化和正式发布
  • 更丰富的配置选项和更灵活的缓存控制
  • unstable_cache 功能的整合或替代

2. 更智能的自动缓存

未来版本的 Next.js 可能会引入更智能的自动缓存机制:

  • 基于使用模式的自适应缓存策略
  • 更精细的缓存粒度控制
  • 基于机器学习的预测性缓存

3. 更强大的缓存管理工具

随着应用规模的增长,缓存管理变得越来越复杂。未来可能会出现:

  • 更完善的缓存监控和分析工具
  • 可视化的缓存管理界面
  • 更灵活的缓存失效策略

4. 与边缘计算的深度集成

随着边缘计算的普及,Next.js 的缓存机制可能会与边缘函数和边缘缓存更紧密地集成:

  • 在边缘节点进行智能缓存决策
  • 基于地理位置和用户特征的个性化缓存策略
  • 边缘缓存和服务器缓存的协同工作

总结

Next.js 提供了四种强大的缓存机制,每种机制都有其独特的优势和适用场景:

  1. 扩展的 fetch API:简单直接,适合缓存 HTTP 请求
  2. unstable_cache 函数:灵活强大,适合缓存复杂函数结果
  3. use cache 指令:简洁明了,适合缓存组件和路由
  4. React 的 cache 函数:轻量级,适合请求内数据共享

在实际应用中,选择合适的缓存机制或组合使用多种机制是提高应用性能的关键。以下是选择缓存策略的一般指南:

  • 对于简单的 API 请求,优先使用扩展的 fetch API
  • 对于需要组合多个数据源的场景,考虑使用 unstable_cache
  • 对于整个组件或路由的缓存,尝试使用 use cache 指令
  • 对于单个请求内的数据共享,使用 React 的 cache 函数

最后,缓存虽然强大,但也需要谨慎使用。过度缓存可能导致数据不一致,而缓存不足则可能影响性能。因此,根据应用的具体需求,设计合理的缓存策略至关重要。

通过深入理解 Next.js 的缓存机制,开发者可以构建出既快速响应又数据新鲜的现代 Web 应用,为用户提供最佳的体验。


希望这篇文章对你理解 Next.js 的数据缓存机制有所帮助。随着技术的不断发展,缓存策略也将持续演进,但掌握这些基本概念和机制将帮助你在不同场景下做出明智的选择。