mcpField
N
nextjs-caching

nextjs-caching

github.com/josiahsiegel开发5972 次安装19 周前更新
代码仓库

关于 nextjs-caching

面向配置缓存与重新验证的 Next.js 开发者参考技能,介绍各缓存层、fetch 策略和按需失效,并提供排查缓存行为的示例。

主要功能

  1. 01Next.js 的四种缓存层
  2. 02fetch 缓存与重新验证选项
  3. 03按时间和标签失效
  4. 04静态生成与 ISR 指引
  5. 05缓存调试方法

适用场景

  1. 01为 fetch 调用选择缓存策略
  2. 02数据变更后重新验证内容
  3. 03配置静态页面与 ISR
  4. 04排查过期或异常缓存数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill nextjs-caching

安装后在 Claude Code 中输入/nextjs-caching确认已加载。

详细介绍

快速参考

缓存层位置时长用途
请求记忆化服务端每次请求对相同的请求进行去重
数据缓存服务端持久存储 fetch 结果
完整路由缓存服务端持久预渲染的 HTML/RSC
路由器缓存客户端会话期间客户端导航
Next.js 16:'use cache' 指令
指令范围使用场景
'use cache'默认缓存通用缓存
'use cache: remote'CDN/边缘缓存静态共享内容
'use cache: private'按用户缓存用户专属数据
缓存函数代码用途
cacheLife('hours')时长设置缓存生命周期
cacheTag('posts')标记启用定向重新验证
Fetch 选项
Fetch 选项代码效果
默认fetch(url)无限期缓存
不缓存{ cache: 'no-store' }始终获取最新内容
重新验证{ next: { revalidate: 60 } }60 秒后过期
标签{ next: { tags: ['posts'] } }添加标签以便失效
重新验证代码使用场景
revalidatePath('/posts')路径修改后
revalidateTag('posts')标签使带标签的数据失效
router.refresh()客户端刷新当前路由

何时使用此技能

用于缓存和重新验证:

  • 了解 Next.js 的缓存行为
  • 配置 fetch 缓存策略
  • 设置增量静态再生成(ISR)
  • 实现按需重新验证
  • 调试缓存问题

相关技能:

  • 关于数据获取:请参阅 nextjs-data-fetching
  • 关于 Server Actions:请参阅 nextjs-server-actions
  • 关于静态生成:请参阅 nextjs-deployment

Next.js 缓存

缓存概览

Next.js 有四种缓存机制:

机制内容位置用途时长
请求记忆化函数的返回值服务端避免组件树中的重复请求每次请求
数据缓存数据服务端在多个请求和部署之间存储数据持久
完整路由缓存HTML 和 RSC 负载服务端降低渲染成本持久
路由器缓存RSC 负载客户端减少导航时的服务端请求会话期间

请求记忆化

自动去重
// This is automatically memoized - only one fetch happens
async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}

// Layout uses getUser
export default async function Layout({ children }: { children: React.ReactNode }) {
  const user = await getUser('1'); // First call - fetches
  return <div>{children}</div>;
}

// Page also uses getUser
export default async function Page() {
  const user = await getUser('1'); // Second call - uses cached result
  return <div>{user.name}</div>;
}
使用 cache() 手动记忆化
import { cache } from 'react';

// For non-fetch functions like database queries
export const getUser = cache(async (id: string) => {
  return db.users.findUnique({ where: { id } });
});

// Both will use the same cached result
const user1 = await getUser('1');
const user2 = await getUser('1');

数据缓存

使用 fetch() 缓存
// Cached indefinitely (default)
const data = await fetch('https://api.example.com/data');

// Opt out of caching
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// Time-based revalidation
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }, // Revalidate every hour
});

// Tag-based caching
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});
对非 fetch 操作使用 unstable_cache
import { unstable_cache } from 'next/cache';

const getCachedUser = unstable_cache(
  async (userId: string) => {
    return db.users.findUnique({ where: { id: userId } });
  },
  ['user'], // Cache key parts
  {
    revalidate: 3600, // 1 hour
    tags: ['users'],
  }
);

export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await getCachedUser(params.id);
  return <div>{user?.name}</div>;
}
路由段配置
// Opt entire route out of caching
export const dynamic = 'force-dynamic';

// Set revalidation for entire route
export const revalidate = 60; // seconds

// Disable cache for all fetches in route
export const fetchCache = 'force-no-store';

// Force static generation
export const dynamic = 'force-static';

重新验证

基于时间的重新验证
// Fetch with revalidation
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }, // Revalidate after 60 seconds
});

// Route-level revalidation
export const revalidate = 60;
按需重新验证
// app/actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

// Revalidate a specific path
export async function updatePost(id: string, data: FormData) {
  await db.posts.update({ where: { id }, data: { /* ... */ } });

  // Revalidate the specific post page
  revalidatePath(`/posts/${id}`);

  // Revalidate the posts list
  revalidatePath('/posts');
}

// Revalidate by tag
export async function createPost(data: FormData) {
  await db.posts.create({ data: { /* ... */ } });

  // All fetches with 'posts' tag will be revalidated
  revalidateTag('posts');
}
重新验证 API 路由
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidate-secret');

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  const body = await request.json();

  if (body.tag) {
    revalidateTag(body.tag);
  }

  if (body.path) {
    revalidatePath(body.path);
  }

  return NextResponse.json({ revalidated: true, now: Date.now() });
}
基于标签的缓存
// Fetch with tags
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts', 'content'] },
  });
  return res.json();
}

async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { tags: ['posts', `post-${id}`] },
  });
  return res.json();
}

// Revalidate all posts
revalidateTag('posts');

// Revalidate specific post
revalidateTag('post-123');

// Revalidate all content
revalidateTag('content');

完整路由缓存

静态路由
// Automatically cached at build time
export default async function StaticPage() {
  const data = await getData();
  return <div>{data}</div>;
}

// Force static
export const dynamic = 'force-static';
使用 generateStaticParams 的动态路由
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await db.posts.findMany({ select: { slug: true } });
  return posts.map((post) => ({ slug: post.slug }));
}

// Pages are pre-rendered at build time
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}
选择退出
// Dynamic route - not cached
export const dynamic = 'force-dynamic';

// Or use dynamic functions
import { headers, cookies } from 'next/headers';

export default async function Page() {
  const headersList = await headers(); // Makes route dynamic
  return <div>...</div>;
}

路由器缓存(客户端)

预取
import Link from 'next/link';

// Prefetch on hover (default)
<Link href="/about">About</Link>

// Disable prefetching
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
使路由器缓存失效
'use client';

import { useRouter } from 'next/navigation';

export function RefreshButton() {
  const router = useRouter();

  const handleRefresh = () => {
    // Refresh current route and invalidate cache
    router.refresh();
  };

  return <button onClick={handleRefresh}>Refresh</button>;
}
revalidatePath 和路由器缓存
// Server Action
'use server';

import { revalidatePath } from 'next/cache';

export async function updateData() {
  // Update data in database
  await db.data.update({ /* ... */ });

  // This also clears the Router Cache for this path
  revalidatePath('/data');
}

缓存标头

路由处理程序缓存
// app/api/data/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const data = await fetchData();

  return NextResponse.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
    },
  });
}
动态路由中的 generateStaticParams
// Generate some pages statically, allow others dynamically
export async function generateStaticParams() {
  // Only pre-render the top 100 posts
  const posts = await db.posts.findMany({ take: 100 });
  return posts.map((post) => ({ slug: post.slug }));
}

// Allow dynamic rendering for posts not in generateStaticParams
export const dynamicParams = true; // default

// Or 404 for unknown slugs
export const dynamicParams = false;

缓存策略

先返回过期内容,再重新验证模式
// Serve cached content while revalidating in background
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 },
});

// First request after 60s: serves stale, triggers revalidation
// Subsequent requests: serve fresh data
缓存优先策略
// Cache indefinitely, revalidate on-demand
const data = await fetch('https://api.example.com/static-data');
// Use revalidateTag('static-data') when data changes
网络优先策略
// Always fetch fresh data
const data = await fetch('https://api.example.com/realtime', {
  cache: 'no-store',
});

调试缓存

缓存状态标头
// Enable cache debugging
// next.config.js
module.exports = {
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
};
检查缓存命中/未命中
// In development, check terminal for cache status
// HIT - served from cache
// MISS - fetched from origin
// STALE - served stale, revalidating

最佳实践

实践描述
使用标签关联相关数据使用标签对相关的 fetch 进行分组
尽量减少重新验证仅重新验证发生变化的内容
优先使用静态生成尽可能使用 generateStaticParams
缓存数据库查询对非 fetch 操作使用 unstable_cache
设置合适的 TTL平衡内容新鲜度与性能
对内容使用 ISR内容型网站使用基于时间的重新验证

评论

支持 Markdown