nextjs-performance-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
This document aggregates three core architectural patterns for modern Next.js 16+ development: Data Fetching Colocation, The Donut Pattern, and Cache Components with
use cache
. These patterns are designed to improve performance, maintainability, and code composition.
Prerequisites: Next.js 16+ with
cacheComponents: true
enabled in
next.config.ts
.

本文档汇总了现代Next.js 16+开发中的三种核心架构模式:数据获取就近原则(Data Fetching Colocation)甜甜圈模式(The Donut Pattern)以及使用
use cache
缓存组件
。这些模式旨在提升性能、可维护性和代码组合性。
前置要求:Next.js 16+版本,且在
next.config.ts
中启用
cacheComponents: true

Quick Decision Guide

快速决策指南

Use this flowchart to choose the right pattern:
┌─────────────────────────────────────────────────────────────────┐
│                    Component Rendering Decision                  │
└─────────────────────────────────────────────────────────────────┘
                ┌─────────────────────────────┐
                │ Does it need user state,    │
                │ event handlers, or hooks?   │
                └─────────────────────────────┘
                     │              │
                   Yes              No
                     │              │
                     ▼              ▼
            ┌────────────┐   ┌─────────────────────┐
            │ "use       │   │ Keep as Server      │
            │ client"    │   │ Component           │
            └────────────┘   └─────────────────────┘
                      ┌─────────────────────────────┐
                      │ Does it fetch data or do    │
                      │ expensive computation?      │
                      └─────────────────────────────┘
                           │              │
                         Yes              No
                           │              │
                           ▼              ▼
            ┌──────────────────────┐   Static in shell
            │ Is data user/request │   (automatic)
            │ specific?            │
            └──────────────────────┘
                  │         │
                Yes         No
                  │         │
                  ▼         ▼
         ┌─────────────┐  ┌─────────────┐
         │ Wrap in     │  │ Add         │
         │ <Suspense>  │  │ "use cache" │
         └─────────────┘  └─────────────┘

使用以下流程图选择合适的模式:
┌─────────────────────────────────────────────────────────────────┐
│                    Component Rendering Decision                  │
└─────────────────────────────────────────────────────────────────┘
                ┌─────────────────────────────┐
                │ Does it need user state,    │
                │ event handlers, or hooks?   │
                └─────────────────────────────┘
                     │              │
                   Yes              No
                     │              │
                     ▼              ▼
            ┌────────────┐   ┌─────────────────────┐
            │ "use       │   │ Keep as Server      │
            │ client"    │   │ Component           │
            └────────────┘   └─────────────────────┘
                      ┌─────────────────────────────┐
                      │ Does it fetch data or do    │
                      │ expensive computation?      │
                      └─────────────────────────────┘
                           │              │
                         Yes              No
                           │              │
                           ▼              ▼
            ┌──────────────────────┐   Static in shell
            │ Is data user/request │   (automatic)
            │ specific?            │
            └──────────────────────┘
                  │         │
                Yes         No
                  │         │
                  ▼         ▼
         ┌─────────────┐  ┌─────────────┐
         │ Wrap in     │  │ Add         │
         │ <Suspense>  │  │ "use cache" │
         └─────────────┘  └─────────────┘

1. Data Fetching Colocation

1. 数据获取就近原则(Data Fetching Colocation)

When to Use

适用场景

  • Data is passed through multiple layers of components (prop drilling)
  • Root layout/page is blocked by a large initial data fetch
  • Components are not reusable because they depend on props from a specific parent
  • 数据需要通过多层组件传递(属性穿透)
  • 根布局/页面被大型初始数据请求阻塞
  • 组件因依赖特定父组件的属性而无法复用

Implementation

实现方式

Move
async
fetch calls directly into the Server Component that consumes the data:
tsx
// ❌ Before: Prop drilling blocks parallelism
export default async function Page() {
  const data = await getData();
  return <Child data={data} />;
}

// ✅ After: Collocated fetching enables parallel loading
export default async function Child() {
  const data = await getData();
  return <div>{data.title}</div>;
}
async
数据请求直接移至消费数据的Server Component中:
tsx
// ❌ 优化前:属性穿透阻塞并行加载
export default async function Page() {
  const data = await getData();
  return <Child data={data} />;
}

// ✅ 优化后:就近获取实现并行加载
export default async function Child() {
  const data = await getData();
  return <div>{data.title}</div>;
}

Resolving Promises with
use()

使用
use()
解析Promise

Pass promises directly to Client Components and unwrap with
React.use()
:
tsx
// Server Component
export default function Page() {
  const userPromise = getUser(); // Don't await!
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// Client Component
"use client";
import { use } from "react";

export function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // Suspends until resolved
  return <div>{user.name}</div>;
}
将Promise直接传递给Client Component,并通过
React.use()
解析:
tsx
// Server Component
export default function Page() {
  const userPromise = getUser(); // 不要await!
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// Client Component
"use client";
import { use } from "react";

export function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // 挂起直到解析完成
  return <div>{user.name}</div>;
}

❌ Anti-Patterns

❌ 反模式

  • Fetching all data at page level and threading through props
  • Using
    useEffect
    +
    useState
    for data that could be fetched server-side
  • Duplicating fetch logic across components instead of colocating

  • 在页面层级获取所有数据并通过属性层层传递
  • 对可在服务端获取的数据使用
    useEffect
    +
    useState
  • 跨组件重复实现请求逻辑而非就近处理

2. The Donut Pattern

2. 甜甜圈模式(The Donut Pattern)

When to Use

适用场景

  • Adding interactivity to a page section while keeping nested content server-rendered
  • Avoiding
    "use client"
    on a large component tree for a small interactive element
  • Preserving
    async
    capability in deeply nested Server Components
  • 为页面某部分添加交互性,同时保持嵌套内容为服务端渲染
  • 避免只为一个小型交互元素就给大型组件树添加
    "use client"
  • 在深度嵌套的Server Component中保留
    async
    能力

Implementation

实现方式

  1. Isolate Interactive Logic → Extract into a Client Component
  2. Create the "Hole" → Accept
    children
    as a prop
  3. Compose on Server → Pass Server Components as children
tsx
// AnimatedContainer.tsx (Client Component - the "donut")
"use client";
import { motion } from "framer-motion";

export function AnimatedContainer({ children }: { children: React.ReactNode }) {
  return (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
      {children}
    </motion.div>
  );
}

// Page.tsx (Server Component)
import { AnimatedContainer } from "./AnimatedContainer";
import { ProductList } from "./ProductList"; // Server Component

export default function Page() {
  return (
    <AnimatedContainer>
      {/* ProductList runs on server, not included in client bundle */}
      <ProductList />
    </AnimatedContainer>
  );
}
  1. 隔离交互逻辑 → 提取为Client Component
  2. 创建"孔洞" → 接受
    children
    作为属性
  3. 服务端组合 → 将Server Component作为子组件传入
tsx
// AnimatedContainer.tsx(Client Component - 即"甜甜圈")
"use client";
import { motion } from "framer-motion";

export function AnimatedContainer({ children }: { children: React.ReactNode }) {
  return (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
      {children}
    </motion.div>
  );
}

// Page.tsx(Server Component)
import { AnimatedContainer } from "./AnimatedContainer";
import { ProductList } from "./ProductList"; // Server Component

export default function Page() {
  return (
    <AnimatedContainer>
      {/* ProductList在服务端运行,不包含在客户端包中 */}
      <ProductList />
    </AnimatedContainer>
  );
}

Benefits

优势

  • Reduced Bundle Size: Server Component code stays on server
  • Async Support: Inner components can still be
    async
    and fetch data
  • Animation/Interactivity: Outer wrapper handles client-side concerns
  • 减小包体积:Server Component代码保留在服务端
  • 支持异步:内部组件仍可作为
    async
    组件并获取数据
  • 动画/交互性:外层包装处理客户端相关逻辑

❌ Anti-Patterns

❌ 反模式

  • Marking entire page as
    "use client"
    to add one click handler
  • Putting data fetching in Client Components when it could be server-side
  • Nesting Client Components unnecessarily deep

  • 为添加一个点击事件就将整个页面标记为
    "use client"
  • 可在服务端获取的数据却放在Client Component中
  • 不必要地深度嵌套Client Component

3. Cache Components with
use cache

3. 使用
use cache
缓存组件

Cache Components let you mix static, cached, and dynamic content in a single route—the speed of static sites with the flexibility of dynamic rendering.
缓存组件允许你在单个路由中混合静态、缓存和动态内容——兼具静态站点的速度和动态渲染的灵活性。

Setup

配置

Enable in
next.config.ts
:
ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
next.config.ts
中启用:
ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

How It Works

工作原理

At build time, Next.js renders your route. Components that don't access network resources or request data are automatically static. For others, you choose:
ScenarioSolution
Needs request data (cookies, headers, user-specific)Wrap in
<Suspense>
Expensive but static/sharedAdd
"use cache"
Mix of bothCombine patterns (Donut + Cache)
在构建时,Next.js会渲染你的路由。不访问网络资源或请求数据的组件会自动变为静态组件。对于其他组件,你可以自主选择:
场景解决方案
需要请求数据(Cookie、请求头、用户特定数据)
<Suspense>
包裹
计算成本高但静态/共享添加
"use cache"
混合场景组合多种模式(甜甜圈+缓存)

Basic Usage

基础用法

tsx
// File-level caching (all exports cached)
"use cache";

export async function getProducts() {
  return await db.product.findMany();
}

export async function getCategories() {
  return await db.category.findMany();
}
tsx
// Function-level caching
export async function getFeaturedSkills() {
  "use cache";
  return await db.skill.findMany({ where: { featured: true } });
}
tsx
// 文件级缓存(所有导出内容均被缓存)
"use cache";

export async function getProducts() {
  return await db.product.findMany();
}

export async function getCategories() {
  return await db.category.findMany();
}
tsx
// 函数级缓存
export async function getFeaturedSkills() {
  "use cache";
  return await db.skill.findMany({ where: { featured: true } });
}

Cache Lifetime with
cacheLife()

使用
cacheLife()
控制缓存生命周期

Control how long cached content lives using preset profiles:
ProfileUse CaseStaleRevalidateExpire
"seconds"
Real-time data (stock prices)0s1s60s
"minutes"
Frequently updated (feeds)5min1min1h
"hours"
Moderately static (blog posts)5min1h1d
"days"
Rarely changing (product catalog)5min1d1w
"weeks"
Very stable (landing pages)5min1w1mo
"max"
Immutable (versioned assets)5min1yindefinite
tsx
import { cacheLife } from "next/cache";

export async function ProductCatalog() {
  "use cache";
  cacheLife("days"); // Cache for ~1 day

  const products = await db.product.findMany();
  return <ProductGrid products={products} />;
}
使用预设配置文件控制缓存内容的有效期:
配置文件适用场景过期后 stale 状态重新验证最终过期
"seconds"
实时数据(股票价格)0s1s60s
"minutes"
频繁更新内容(信息流)5min1min1h
"hours"
中度静态内容(博客文章)5min1h1d
"days"
极少变更内容(产品目录)5min1d1w
"weeks"
高度稳定内容(落地页)5min1w1mo
"max"
不可变内容(版本化资源)5min1y永久
tsx
import { cacheLife } from "next/cache";

export async function ProductCatalog() {
  "use cache";
  cacheLife("days"); // 缓存约1天

  const products = await db.product.findMany();
  return <ProductGrid products={products} />;
}

Conditional Cache Lifetimes

条件式缓存生命周期

tsx
import { cacheLife, cacheTag } from "next/cache";

async function getPostContent(slug: string) {
  "use cache";
  cacheTag(`post-${slug}`);

  const post = await fetchPost(slug);

  if (!post) {
    cacheLife("minutes"); // Missing content, check again soon
    return null;
  }

  cacheLife("days"); // Published content, cache longer
  return post.data;
}
tsx
import { cacheLife, cacheTag } from "next/cache";

async function getPostContent(slug: string) {
  "use cache";
  cacheTag(`post-${slug}`);

  const post = await fetchPost(slug);

  if (!post) {
    cacheLife("minutes"); // 内容缺失,很快重新检查
    return null;
  }

  cacheLife("days"); // 已发布内容,缓存更久
  return post.data;
}

Cache Invalidation with
cacheTag()

使用
cacheTag()
实现缓存失效

Tag cached entries for on-demand invalidation:
tsx
import { cacheTag } from "next/cache";

export async function getSkillById(id: string) {
  "use cache";
  cacheTag("skills", `skill-${id}`);

  return await db.skill.findUnique({ where: { id } });
}
Invalidate with
updateTag()
in Server Actions (preferred for immediate invalidation):
tsx
"use server";
import { updateTag } from "next/cache";

export async function updateSkill(id: string, data: SkillData) {
  await db.skill.update({ where: { id }, data });
  updateTag(`skill-${id}`); // Invalidate specific skill immediately
  updateTag("skills");       // Invalidate all skills
}
updateTag
vs
revalidateTag
:
  • updateTag
    — Use in Server Actions for read-your-own-writes (user sees changes immediately)
  • revalidateTag
    — Use in Route Handlers, webhooks, or when stale-while-revalidate is acceptable
为缓存条目添加标签以支持按需失效:
tsx
import { cacheTag } from "next/cache";

export async function getSkillById(id: string) {
  "use cache";
  cacheTag("skills", `skill-${id}`);

  return await db.skill.findUnique({ where: { id } });
}
在Server Actions中使用
updateTag()
实现即时失效(推荐方式):
tsx
"use server";
import { updateTag } from "next/cache";

export async function updateSkill(id: string, data: SkillData) {
  await db.skill.update({ where: { id }, data });
  updateTag(`skill-${id}`); // 即时失效特定技能缓存
  updateTag("skills");       // 失效所有技能缓存
}
updateTag
vs
revalidateTag
:
  • updateTag
    — 在Server Actions中使用,实现读写一致性(用户立即看到变更)
  • revalidateTag
    — 在路由处理器、Webhook中使用,或可接受 stale-while-revalidate 策略时使用

❌ Anti-Patterns

❌ 反模式

Don'tDo Instead
export const revalidate = 3600
cacheLife("hours")
inside
"use cache"
export const dynamic = "force-static"
Add
"use cache"
to component
export const fetchCache = "force-cache"
Use
"use cache"
to control caching
Reading cookies/headers inside cached scopeRead outside, pass as arguments

错误做法正确做法
export const revalidate = 3600
"use cache"
作用域内使用
cacheLife("hours")
export const dynamic = "force-static"
为组件添加
"use cache"
export const fetchCache = "force-cache"
使用
"use cache"
控制缓存
在缓存作用域内读取Cookie/请求头在作用域外读取,作为参数传入

4. Combined Patterns

4. 组合模式

The real power comes from combining all three patterns:
真正的威力来自于三种模式的组合使用:

Example: E-commerce Product Page

示例:电商产品页

tsx
// app/products/[id]/page.tsx (Server Component)
import { Suspense } from "react";
import { ProductDetails } from "@/components/ProductDetails";
import { AddToCartButton } from "@/components/AddToCartButton";
import { RecommendedProducts } from "@/components/RecommendedProducts";
import { ProductSkeleton, RecommendedSkeleton } from "@/components/skeletons";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <div className="grid grid-cols-3 gap-6">
      {/* Cached: Product details rarely change */}
      <div className="col-span-2">
        <Suspense fallback={<ProductSkeleton />}>
          <ProductDetails id={id} />
        </Suspense>
      </div>

      {/* Dynamic: User-specific cart state (Donut Pattern) */}
      <aside>
        <AddToCartButton productId={id} />
      </aside>

      {/* Cached with Suspense: Recommendations can stream in */}
      <div className="col-span-3">
        <Suspense fallback={<RecommendedSkeleton />}>
          <RecommendedProducts productId={id} />
        </Suspense>
      </div>
    </div>
  );
}
tsx
// components/ProductDetails.tsx (Cached Server Component)
import { cacheLife, cacheTag } from "next/cache";

export async function ProductDetails({ id }: { id: string }) {
  "use cache";
  cacheLife("hours");
  cacheTag("products", `product-${id}`);

  const product = await db.product.findUnique({ where: { id } });
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span className="text-2xl font-bold">${product.price}</span>
    </article>
  );
}
tsx
// components/AddToCartButton.tsx (Client Component - Donut wrapper)
"use client";
import { useState, useTransition } from "react";
import { addToCart } from "@/actions/cart";

export function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition();
  const [quantity, setQuantity] = useState(1);

  return (
    <form
      action={() => startTransition(() => addToCart(productId, quantity))}
    >
      <input
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        min={1}
      />
      <button disabled={isPending}>
        {isPending ? "Adding..." : "Add to Cart"}
      </button>
    </form>
  );
}

tsx
// app/products/[id]/page.tsx(Server Component)
import { Suspense } from "react";
import { ProductDetails } from "@/components/ProductDetails";
import { AddToCartButton } from "@/components/AddToCartButton";
import { RecommendedProducts } from "@/components/RecommendedProducts";
import { ProductSkeleton, RecommendedSkeleton } from "@/components/skeletons";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <div className="grid grid-cols-3 gap-6">
      {/* 缓存:产品详情极少变更 */}
      <div className="col-span-2">
        <Suspense fallback={<ProductSkeleton />}>
          <ProductDetails id={id} />
        </Suspense>
      </div>

      {/* 动态:用户特定购物车状态(甜甜圈模式) */}
      <aside>
        <AddToCartButton productId={id} />
      </aside>

      {/* 带Suspense的缓存:推荐商品可流式加载 */}
      <div className="col-span-3">
        <Suspense fallback={<RecommendedSkeleton />}>
          <RecommendedProducts productId={id} />
        </Suspense>
      </div>
    </div>
  );
}
tsx
// components/ProductDetails.tsx(缓存的Server Component)
import { cacheLife, cacheTag } from "next/cache";

export async function ProductDetails({ id }: { id: string }) {
  "use cache";
  cacheLife("hours");
  cacheTag("products", `product-${id}`);

  const product = await db.product.findUnique({ where: { id } });
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span className="text-2xl font-bold">${product.price}</span>
    </article>
  );
}
tsx
// components/AddToCartButton.tsx(Client Component - 甜甜圈包装器)
"use client";
import { useState, useTransition } from "react";
import { addToCart } from "@/actions/cart";

export function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition();
  const [quantity, setQuantity] = useState(1);

  return (
    <form
      action={() => startTransition(() => addToCart(productId, quantity))}
    >
      <input
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        min={1}
      />
      <button disabled={isPending}>
        {isPending ? "添加中..." : "加入购物车"}
      </button>
    </form>
  );
}

5. Suspense Boundaries Best Practices

5. Suspense边界最佳实践

When to Use Suspense

何时使用Suspense

ScenarioSuspense Needed?
Cached component (
"use cache"
)
Usually not needed (part of static shell)
Dynamic data (user-specific)Yes - shows fallback while loading
Streaming async Server ComponentYes - prevents blocking
Client Component with
use()
Yes - parent must provide boundary
场景是否需要Suspense?
缓存组件(
"use cache"
通常不需要(属于静态外壳的一部分)
动态数据(用户特定) - 加载时显示占位内容
流式异步Server Component - 防止阻塞页面渲染
使用
use()
的Client Component
- 父组件必须提供边界

Granular vs. Coarse Boundaries

细粒度 vs 粗粒度边界

tsx
// ❌ Coarse: Entire page waits for all data
<Suspense fallback={<FullPageSkeleton />}>
  <Header />
  <MainContent />
  <Sidebar />
</Suspense>

// ✅ Granular: Components load independently
<Header /> {/* Static, no Suspense */}
<Suspense fallback={<ContentSkeleton />}>
  <MainContent /> {/* Async */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
  <Sidebar /> {/* Async, loads parallel to MainContent */}
</Suspense>

tsx
// ❌ 粗粒度:整个页面等待所有数据加载
<Suspense fallback={<FullPageSkeleton />}>
  <Header />
  <MainContent />
  <Sidebar />
</Suspense>

// ✅ 细粒度:组件独立加载
<Header /> {/* 静态内容,无需Suspense */}
<Suspense fallback={<ContentSkeleton />}>
  <MainContent /> {/* 异步组件 */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
  <Sidebar /> {/* 异步组件,与MainContent并行加载 */}
</Suspense>

6. Debugging Tips

6. 调试技巧

Check Cache Status

检查缓存状态

In development, Next.js logs cache hits/misses. Look for:
  • CACHE HIT
    - Served from cache
  • CACHE MISS
    - Generated fresh and cached
  • CACHE SKIP
    - Not cacheable (dynamic data accessed)
在开发环境中,Next.js会记录缓存命中/未命中情况,留意以下日志:
  • CACHE HIT
    - 从缓存中返回
  • CACHE MISS
    - 重新生成并缓存
  • CACHE SKIP
    - 不可缓存(访问了动态数据)

Common Issues

常见问题

SymptomLikely CauseFix
Component not cachingAccessing request-specific dataMove cookies/headers read outside cached scope
Stale data after mutationMissing
revalidateTag
call
Add proper cache tags and revalidate
Hydration mismatchDate/time in cached componentUse
cacheLife("seconds")
or move to client
Build error with
use cache
Edge runtime not supportedUse Node.js runtime only
症状可能原因修复方案
组件未被缓存访问了请求特定数据将Cookie/请求头读取逻辑移至缓存作用域外
数据变更后仍显示旧内容缺少
revalidateTag
调用
添加正确的缓存标签并执行重新验证
hydration不匹配缓存组件中包含日期/时间使用
cacheLife("seconds")
或移至客户端处理
使用
use cache
时构建报错
不支持Edge运行时仅使用Node.js运行时

Verify Static Shell

验证静态外壳

Run
next build
and check the output:
  • = Static (rendered at build time)
  • = SSG with dynamic params
  • ƒ
    = Dynamic (rendered at request time)
  • = Partial Prerendering (static shell + dynamic holes)

运行
next build
并检查输出:
  • = 静态内容(构建时渲染)
  • = 带动态参数的SSG
  • ƒ
    = 动态内容(请求时渲染)
  • = 部分预渲染(静态外壳+动态孔洞)

Quick Reference

快速参考

PatternPurposeKey Directive
Data ColocationFetch where data is usedNone (architectural)
Donut PatternServer content in Client wrapper
"use client"
on wrapper only
Cache ComponentsCache expensive computations
"use cache"
FunctionPurpose
cacheLife(profile)
Set cache duration
cacheTag(...tags)
Tag for targeted invalidation
updateTag(tag)
Invalidate immediately (Server Actions only)
revalidateTag(tag)
Invalidate with stale-while-revalidate (Route Handlers, webhooks)

模式用途核心指令
数据就近获取在使用数据的位置获取数据无(架构层面)
甜甜圈模式在客户端包装器中保留服务端内容仅在包装器上添加
"use client"
缓存组件缓存高成本计算
"use cache"
函数用途
cacheLife(profile)
设置缓存时长
cacheTag(...tags)
为定向失效添加标签
updateTag(tag)
即时失效(仅Server Actions可用)
revalidateTag(tag)
带stale-while-revalidate的失效(路由处理器、Webhook可用)

Related Documentation

相关文档