nextjs-performance-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThis document aggregates three core architectural patterns for modern Next.js 16+ development: Data Fetching Colocation, The Donut Pattern, and Cache Components with . These patterns are designed to improve performance, maintainability, and code composition.
use cachePrerequisites: Next.js 16+ withenabled incacheComponents: true.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 fetch calls directly into the Server Component that consumes the data:
asynctsx
// ❌ 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>;
}将数据请求直接移至消费数据的Server Component中:
asynctsx
// ❌ 优化前:属性穿透阻塞并行加载
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()使用use()
解析Promise
use()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 +
useEffectfor data that could be fetched server-sideuseState - Duplicating fetch logic across components instead of colocating
- 在页面层级获取所有数据并通过属性层层传递
- 对可在服务端获取的数据使用+
useEffectuseState - 跨组件重复实现请求逻辑而非就近处理
2. The Donut Pattern
2. 甜甜圈模式(The Donut Pattern)
When to Use
适用场景
- Adding interactivity to a page section while keeping nested content server-rendered
- Avoiding on a large component tree for a small interactive element
"use client" - Preserving capability in deeply nested Server Components
async
- 为页面某部分添加交互性,同时保持嵌套内容为服务端渲染
- 避免只为一个小型交互元素就给大型组件树添加
"use client" - 在深度嵌套的Server Component中保留能力
async
Implementation
实现方式
- Isolate Interactive Logic → Extract into a Client Component
- Create the "Hole" → Accept as a prop
children - 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>
);
}- 隔离交互逻辑 → 提取为Client Component
- 创建"孔洞" → 接受作为属性
children - 服务端组合 → 将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 and fetch data
async - Animation/Interactivity: Outer wrapper handles client-side concerns
- 减小包体积:Server Component代码保留在服务端
- 支持异步:内部组件仍可作为组件并获取数据
async - 动画/交互性:外层包装处理客户端相关逻辑
❌ Anti-Patterns
❌ 反模式
- Marking entire page as to add one click handler
"use client" - 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
use cache3. 使用use cache
缓存组件
use cacheCache 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.tsts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;在中启用:
next.config.tsts
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:
| Scenario | Solution |
|---|---|
| Needs request data (cookies, headers, user-specific) | Wrap in |
| Expensive but static/shared | Add |
| Mix of both | Combine patterns (Donut + Cache) |
在构建时,Next.js会渲染你的路由。不访问网络资源或请求数据的组件会自动变为静态组件。对于其他组件,你可以自主选择:
| 场景 | 解决方案 |
|---|---|
| 需要请求数据(Cookie、请求头、用户特定数据) | 用 |
| 计算成本高但静态/共享 | 添加 |
| 混合场景 | 组合多种模式(甜甜圈+缓存) |
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()使用cacheLife()
控制缓存生命周期
cacheLife()Control how long cached content lives using preset profiles:
| Profile | Use Case | Stale | Revalidate | Expire |
|---|---|---|---|---|
| Real-time data (stock prices) | 0s | 1s | 60s |
| Frequently updated (feeds) | 5min | 1min | 1h |
| Moderately static (blog posts) | 5min | 1h | 1d |
| Rarely changing (product catalog) | 5min | 1d | 1w |
| Very stable (landing pages) | 5min | 1w | 1mo |
| Immutable (versioned assets) | 5min | 1y | indefinite |
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 状态 | 重新验证 | 最终过期 |
|---|---|---|---|---|
| 实时数据(股票价格) | 0s | 1s | 60s |
| 频繁更新内容(信息流) | 5min | 1min | 1h |
| 中度静态内容(博客文章) | 5min | 1h | 1d |
| 极少变更内容(产品目录) | 5min | 1d | 1w |
| 高度稳定内容(落地页) | 5min | 1w | 1mo |
| 不可变内容(版本化资源) | 5min | 1y | 永久 |
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()使用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 in Server Actions (preferred for immediate invalidation):
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}`); // Invalidate specific skill immediately
updateTag("skills"); // Invalidate all skills
}vsupdateTag:revalidateTag
— Use in Server Actions for read-your-own-writes (user sees changes immediately)updateTag — Use in Route Handlers, webhooks, or when stale-while-revalidate is acceptablerevalidateTag
为缓存条目添加标签以支持按需失效:
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"); // 失效所有技能缓存
}vsupdateTag:revalidateTag
— 在Server Actions中使用,实现读写一致性(用户立即看到变更)updateTag — 在路由处理器、Webhook中使用,或可接受 stale-while-revalidate 策略时使用revalidateTag
❌ Anti-Patterns
❌ 反模式
| Don't | Do Instead |
|---|---|
| |
| Add |
| Use |
| Reading cookies/headers inside cached scope | Read outside, pass as arguments |
| 错误做法 | 正确做法 |
|---|---|
| 在 |
| 为组件添加 |
| 使用 |
| 在缓存作用域内读取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
| Scenario | Suspense Needed? |
|---|---|
Cached component ( | Usually not needed (part of static shell) |
| Dynamic data (user-specific) | Yes - shows fallback while loading |
| Streaming async Server Component | Yes - prevents blocking |
Client Component with | Yes - parent must provide boundary |
| 场景 | 是否需要Suspense? |
|---|---|
缓存组件( | 通常不需要(属于静态外壳的一部分) |
| 动态数据(用户特定) | 是 - 加载时显示占位内容 |
| 流式异步Server 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:
- - Served from cache
CACHE HIT - - Generated fresh and cached
CACHE MISS - - Not cacheable (dynamic data accessed)
CACHE SKIP
在开发环境中,Next.js会记录缓存命中/未命中情况,留意以下日志:
- - 从缓存中返回
CACHE HIT - - 重新生成并缓存
CACHE MISS - - 不可缓存(访问了动态数据)
CACHE SKIP
Common Issues
常见问题
| Symptom | Likely Cause | Fix |
|---|---|---|
| Component not caching | Accessing request-specific data | Move cookies/headers read outside cached scope |
| Stale data after mutation | Missing | Add proper cache tags and revalidate |
| Hydration mismatch | Date/time in cached component | Use |
Build error with | Edge runtime not supported | Use Node.js runtime only |
| 症状 | 可能原因 | 修复方案 |
|---|---|---|
| 组件未被缓存 | 访问了请求特定数据 | 将Cookie/请求头读取逻辑移至缓存作用域外 |
| 数据变更后仍显示旧内容 | 缺少 | 添加正确的缓存标签并执行重新验证 |
| hydration不匹配 | 缓存组件中包含日期/时间 | 使用 |
使用 | 不支持Edge运行时 | 仅使用Node.js运行时 |
Verify Static Shell
验证静态外壳
Run and check the output:
next build- = 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
快速参考
| Pattern | Purpose | Key Directive |
|---|---|---|
| Data Colocation | Fetch where data is used | None (architectural) |
| Donut Pattern | Server content in Client wrapper | |
| Cache Components | Cache expensive computations | |
| Function | Purpose |
|---|---|
| Set cache duration |
| Tag for targeted invalidation |
| Invalidate immediately (Server Actions only) |
| Invalidate with stale-while-revalidate (Route Handlers, webhooks) |
| 模式 | 用途 | 核心指令 |
|---|---|---|
| 数据就近获取 | 在使用数据的位置获取数据 | 无(架构层面) |
| 甜甜圈模式 | 在客户端包装器中保留服务端内容 | 仅在包装器上添加 |
| 缓存组件 | 缓存高成本计算 | |
| 函数 | 用途 |
|---|---|
| 设置缓存时长 |
| 为定向失效添加标签 |
| 即时失效(仅Server Actions可用) |
| 带stale-while-revalidate的失效(路由处理器、Webhook可用) |