一、概览与提问(SQ3R · Survey & Question)

SQ3R 第一步:快速浏览全貌,提出关键问题。

什么是 Next.js?

Next.js 是由 Vercel 开发和维护的 React 全栈框架。它不仅仅是一个前端库的封装,而是一套完整的应用开发解决方案——从路由、渲染、数据获取到部署,覆盖了 Web 应用开发的全部生命周期。

Next.js 诞生的背景很明确:React 本身只是一个 UI 库,要构建一个完整的 Web 应用,开发者还需要自己处理路由、服务端渲染、代码分割、数据获取等一系列问题。Next.js 的出现就是为了填补这些空白,让开发者能用 React 快速构建生产级别的应用。

当前最新版本为 Next.js 16(基于文档版本 16.2.6),引入了 Cache Components、Partial Prerendering(PPR) 等重大特性,架构模型发生了根本性变化。

核心问题

  • Next.js 适合什么场景?——从静态博客到动态 SaaS 平台,从内容站到电商平台,Next.js 都能胜任
  • Next.js 与同类技术相比有什么优势?——内置 SSR/SSG/ISR、文件系统路由、自动代码分割、React Server Components 原生支持
  • 学习 Next.js 需要什么基础?——HTML、CSS、JavaScript 和 React 基本知识

技术全景图

Next.js 的架构可以看作一棵分层树:

Next.js 应用
├── App Router (/app)              ← 推荐的新路由系统
│   ├── 布局系统 (layout.js)       ← 共享布局,不会重新渲染
│   ├── 页面系统 (page.js)         ← 路由入口,每个路由一个
│   ├── 加载状态 (loading.js)      ← 自动 Suspense 边界
│   ├── 错误处理 (error.js)        ← 错误边界
│   ├── API 路由 (route.js)        ← 服务端 API 端点
│   └── 特殊文件 (not-found.js 等)
├── Server Components              ← 默认,服务端渲染,零 JS 发送到客户端
├── Client Components ("use client") ← 客户端交互,需要时使用
├── Cache Components ("use cache")   ← Next.js 16 新增,缓存策略
├── 数据获取 (fetch / ORM)         ← Server Components 中直接 await
├── Server Actions ("use server")  ← 服务端函数,表单提交
└── next.config.ts                 ← 全局配置

二、用最简单的话说清楚(费曼学习法)

费曼学习法核心理念:如果你不能用简单的语言解释一件事,说明你还没有真正理解它。

核心概念讲解

1. 文件系统路由

Next.js 的路由完全由文件夹结构决定。你在 app/ 目录下创建一个文件夹,它就是一个路由。文件夹里放一个 page.js,它就是这个路由的页面。

// app/page.tsx → 对应路由 /
export default function HomePage() {
  return <h1>这是首页</h1>;
}
 
// app/about/page.tsx → 对应路由 /about
export default function AboutPage() {
  return <h1>关于我们</h1>;
}
 
// app/blog/[id]/page.tsx → 对应路由 /blog/123(动态路由)
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <h1>文章 {id}</h1>;
}

不需要写任何路由配置文件,文件夹结构就是路由表。

2. 布局(Layout)

布局是一个共享的外壳,它包裹着所有子页面。最关键的是:导航时布局不会重新渲染。

// app/layout.tsx → 根布局(必须有 html 和 body 标签)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>
        <nav>导航栏(导航时不重新渲染)</nav>
        <main>{children}</main>
      </body>
    </html>
  );
}

3. Server Components 和 Client Components

这是 Next.js 最核心的概念。默认情况下,所有组件都是 Server Components——它们在服务端运行,不会发送任何 JavaScript 到客户端。当你需要交互(点击事件、状态管理、浏览器 API)时,才使用 Client Components。

// Server Component(默认)- 在服务端运行,发送 HTML 到客户端
// app/page.tsx
import { db } from "@/lib/db";
import LikeButton from "./like-button";
 
export default async function Page() {
  const posts = await db.query("SELECT * FROM posts"); // 直接访问数据库
  return (
    <div>
      {posts.map((post) => (
        <p key={post.id}>{post.title}</p>
      ))}
      <LikeButton /> {/* 交互部分交给 Client Component */}
    </div>
  );
}
// Client Component - 需要交互时添加 "use client"
// app/like-button.tsx
"use client";
 
import { useState } from "react";
 
export default function LikeButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>赞 {count}</button>;
}

4. 数据获取

在 Server Components 中,数据获取变得极其简单——直接用 async/await。

// Server Component 中直接 fetch
export default async function BlogPage() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
 
  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

不需要 useEffect,不需要 useState,不需要客户端数据获取库。代码就像在写普通的异步函数。

5. 缓存(Next.js 16 新特性)

Next.js 16 引入了 Cache Components,通过 "use cache" 指令让缓存变得声明式。

// next.config.ts 中启用
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
  cacheComponents: true,
};
export default nextConfig;
// 数据级缓存
import { cacheLife } from "next/cache";
 
export async function getUsers() {
  "use cache";
  cacheLife("hours"); // 缓存一小时
  return db.query("SELECT * FROM users");
}
// UI 级缓存
import { cacheLife } from "next/cache";
 
export default async function BlogPosts() {
  "use cache";
  cacheLife("hours");
  const posts = await fetch("https://api.example.com/posts");
  return <div>{/* 渲染文章列表 */}</div>;
}

6. Server Actions

Server Actions 让你直接在组件中定义服务端函数,无需手动写 API 路由。

// app/actions.ts
"use server";
 
import { revalidateTag } from "next/cache";
 
export async function createPost(formData: FormData) {
  await db.post.create({
    data: { title: formData.get("title") as string },
  });
  revalidateTag("posts"); // 刷新缓存
}
// 在表单中使用
import { createPost } from "./actions";
 
export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="文章标题" />
      <button type="submit">发布</button>
    </form>
  );
}

类比与比喻

把 Next.js 应用想象成一家餐厅:

  • Server Components = 后厨厨师——在厨房里准备好菜品(HTML),客人看不到烹饪过程,只能看到端上桌的成品。好处是厨房里可以用大型设备(数据库、文件系统),客人不需要在自己的桌上放这些设备。
  • Client Components = 桌上的调料瓶——需要客人自己动手(交互),比如加酱油(点击按钮)、搅拌(拖动)。
  • Layout = 餐厅装修——不管换哪道菜(页面导航),装修都不会变,不需要重新装修。
  • Cache Components("use cache") = 预制菜——提前做好,客人点了直接上,不用现做。
  • Streaming(Suspense) = 先上凉菜再上热菜——不必等所有菜做好才上桌,做好一道上一道。
  • Server Actions = 服务员点单系统——客人只需填表,服务员自动传到后厨处理。

常见误解澄清

  1. 误解:Next.js 只能做 SSR —— 事实上它支持 SSG、SSR、ISR、CSR 和 PPR(Partial Prerendering),可以在同一个应用中混合使用。
  2. 误解:所有组件都需要 "use client" —— 恰恰相反,默认都是 Server Components,只有需要交互时才加 "use client"。
  3. 误解:Server Components 不能传 props 给 Client Components —— 完全可以,只要 props 是可序列化的(数字、字符串、对象等)。
  4. 误解:Next.js 16 还在用 Pages Router 的 getServerSideProps —— App Router 已经完全不同,数据获取直接 await,不需要这些函数。
  5. 误解:缓存很难控制 —— Next.js 16 的 "use cache" + cacheLife + cacheTag 让缓存变得声明式和可控。

三、锥形深入(西蒙学习法)

西蒙学习法:集中精力、目标导向、锥形深入——从核心开始,逐步扩展到周边。

第一层:核心基础

1. 创建项目

npx create-next-app@latest

安装时会提示选择 TypeScript、ESLint、Tailwind CSS、src/ 目录、App Router 等。

手动安装:

npm install next@latest react@latest react-dom@latest
// package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
}

2. 项目结构

my-app/
├── app/
│   ├── layout.tsx          ← 根布局
│   ├── page.tsx            ← 首页 (/)
│   ├── blog/
│   │   ├── page.tsx        ← /blog
│   │   └── [id]/
│   │       └── page.tsx    ← /blog/123
│   ├── about/
│   │   └── page.tsx        ← /about
│   └── api/
│       └── route.ts        ← API 端点
├── public/                 ← 静态资源
├── next.config.ts          ← 配置文件
├── package.json
└── tsconfig.json

3. 页面和布局

// app/layout.tsx - 根布局(必需)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>{children}</body>
    </html>
  );
}
// app/blog/layout.tsx - 嵌套布局
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <h2>博客</h2>
      {children}
    </section>
  );
}

4. 链接和导航

import Link from "next/link";
 
export default function NavBar() {
  return (
    <nav>
      <Link href="/">首页</Link>
      <Link href="/about">关于</Link>
      <Link href="/blog/123">文章</Link>
    </nav>
  );
}

<Link> 组件会自动预取页面,实现即时导航。

5. 动态路由

// app/blog/[id]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <h1>文章 ID: {id}</h1>;
}

Catch-all 路由:[...slug] 匹配多个路径段,[[...slug]] 是可选的 catch-all。

6. 加载状态

// app/blog/loading.tsx - 自动 Suspense 边界
export default function Loading() {
  return <div>加载中...</div>;
}

当 blog/page.tsx 在服务端获取数据时,用户会立即看到 "加载中...",数据就绪后自动替换。

7. 错误处理

// app/error.tsx - 必须是 Client Component
"use client";
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>出错了!</h2>
      <button onClick={reset}>重试</button>
    </div>
  );
}

8. API 路由(Route Handlers)

// app/api/posts/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  const posts = await db.query("SELECT * FROM posts");
  return NextResponse.json(posts);
}
 
export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.post.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}

第二层:进阶用法

1. Server Components 和 Client Components 的组合模式

最常见的模式是:Server Component 获取数据,通过 props 传递给 Client Component 处理交互。

// app/page.tsx(Server Component)
import LikeButton from "./like-button";
import { getPost } from "@/lib/data";
 
export default async function Page() {
  const post = await getPost("1");
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <LikeButton likes={post.likes} />
    </article>
  );
}

还可以通过 children prop 将 Server Component 嵌套在 Client Component 中:

// app/ui/modal.tsx(Client Component)
"use client";
 
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div className="modal">{children}</div>;
}
// app/page.tsx(Server Component)
import Modal from "./ui/modal";
import Cart from "./ui/cart"; // Cart 是 Server Component
 
export default function Page() {
  return (
    <Modal>
      <Cart /> {/* Server Component 作为 children 传入 Client Component */}
    </Modal>
  );
}

2. Context Providers

React Context 不支持 Server Components,需要在 Client Component 中创建 Provider:

// app/theme-provider.tsx
"use client";
 
import { createContext } from "react";
 
export const ThemeContext = createContext("light");
 
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
// app/layout.tsx
import ThemeProvider from "./theme-provider";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

3. Streaming 和 Suspense

Suspense 是 Next.js 中流式渲染的核心。它让你可以把页面拆分成多个独立加载的块:

import { Suspense } from "react";
 
export default function Page() {
  return (
    <>
      <h1>我的博客</h1>
      {/* 这部分立即显示 */}
      <p>欢迎来到我的博客</p>
      {/* 这部分在数据加载完成后流式显示 */}
      <Suspense fallback={<div>加载文章中...</div>}>
        <BlogPosts />
      </Suspense>
      <Suspense fallback={<div>加载评论中...</div>}>
        <Comments />
      </Suspense>
    </>
  );
}

4. 数据获取模式

并行获取(推荐):

// 多个请求同时发起
export default async function Page() {
  const artistPromise = getArtist();
  const albumsPromise = getAlbums();
 
  const [artist, albums] = await Promise.all([artistPromise, albumsPromise]);
 
  return (
    <div>
      <h1>{artist.name}</h1>
      <AlbumList albums={albums} />
    </div>
  );
}

顺序获取(当请求有依赖关系时):

export default async function Page() {
  const artist = await getArtist(); // 先获取 artist
  const playlists = await getPlaylists(artist.id); // 再用 artist.id 获取
  return <div>{/* ... */}</div>;
}

5. 缓存与 Revalidation(Next.js 16)

Next.js 16 的缓存系统基于 "use cache" 指令:

import { cacheLife, cacheTag } from "next/cache";
 
// 数据级缓存 + 标签
export async function getProducts() {
  "use cache";
  cacheLife("hours"); // 缓存存活时间
  cacheTag("products"); // 缓存标签,用于按需刷新
  return db.query("SELECT * FROM products");
}

时间基准刷新:

"use cache";
cacheLife("seconds"); // 每秒刷新
cacheLife("minutes"); // 每分钟刷新
cacheLife("hours"); // 每小时刷新
cacheLife("days"); // 每天刷新
cacheLife("weeks"); // 每周刷新
cacheLife("max"); // 最大缓存

按需刷新:

// app/actions.ts
"use server";
import { revalidateTag, revalidatePath, updateTag } from "next/cache";
 
export async function refreshProducts() {
  revalidateTag("products"); // 刷新带 'products' 标签的所有缓存
}
 
export async function refreshPage() {
  revalidatePath("/products"); // 刷新 /products 路径的所有缓存
}
 
export async function updateProducts() {
  updateTag("products"); // 立即过期并重新生成
}

6. 运行时 API 与缓存

运行时 API(cookies()、headers()、searchParams)需要请求级别的信息。在 Next.js 16 中,使用它们的组件需要用 <Suspense> 包裹:

import { cookies } from "next/headers";
import { Suspense } from "react";
 
async function UserGreeting() {
  const cookieStore = await cookies();
  const theme = cookieStore.get("theme")?.value || "light";
  return <p>你的主题: {theme}</p>;
}
 
export default function Page() {
  return (
    <>
      <h1>仪表盘</h1>
      <Suspense fallback={<p>加载中...</p>}>
        <UserGreeting />
      </Suspense>
    </>
  );
}

7. Metadata 和 SEO

// app/layout.tsx - 静态 metadata
export const metadata = {
  title: "我的网站",
  description: "这是我的网站描述",
};
 
// app/blog/[id]/page.tsx - 动态 metadata
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const post = await getPost(id);
  return {
    title: post.title,
    description: post.excerpt,
  };
}

第三层:深度解析

1. 渲染架构:Partial Prerendering(PPR)

Next.js 16 的默认渲染模式是 Partial Prerendering。在构建时,Next.js 会预渲染整个组件树,根据每个组件使用的 API 来决定如何处理:

  • "use cache" 的组件 → 结果被缓存,包含在静态 Shell 中
  • <Suspense> 包裹的组件 → fallback 包含在静态 Shell 中,内容在请求时流式传输
  • 纯确定性操作 → 自动包含在静态 Shell 中

这意味着用户访问页面时,会立即看到静态的页面框架(导航栏、布局、缓存的内容),然后个性化内容通过流式传输逐步加载。

2. RSC Payload

React Server Components 渲染后会生成一种特殊的二进制格式——RSC Payload。它包含:

  • Server Components 的渲染结果
  • Client Components 的占位符和 JS 文件引用
  • 从 Server Component 传递给 Client Component 的 props

在首次加载时,HTML 提供即时预览,RSC Payload 用于协调组件树,JavaScript 用于水合 Client Components。后续导航则直接使用 RSC Payload,不需要重新获取 HTML。

3. 环境隔离与安全

Next.js 在服务端和客户端之间建立了严格的安全边界:

  • 环境变量只有 NEXT_PUBLIC_ 前缀的才会暴露给客户端
  • 即使不慎在客户端导入了服务端代码,敏感变量也会被替换为空字符串
  • 可以使用 server-only 包在构建时报错阻止客户端导入服务端代码
  • 可以使用 client-only 包阻止服务端导入客户端代码
import "server-only"; // 阻止客户端导入
 
export async function getData() {
  const res = await fetch("https://api.example.com/data", {
    headers: { authorization: process.env.API_KEY },
  });
  return res.json();
}

4. 减小客户端 JS 体积的最佳实践

// 正确做法:只在需要交互的组件上加 "use client"
// app/layout.tsx - 这是 Server Component
import Search from "./search"; // Client Component
import Logo from "./logo"; // Server Component
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search /> {/* 只有搜索栏是 Client Component */}
      </nav>
      <main>{children}</main>
    </>
  );
}

将 "use client" 尽可能下推到叶子组件,减少客户端 JavaScript 的体积。

5. 共享数据的模式:React.cache + Context

在多个组件间共享数据而不重复请求:

import { cache, createContext } from "react";
 
// 创建缓存函数,同一请求内多次调用只执行一次
const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

React.cache 的作用域是单个请求,不同请求之间不会共享缓存。

6. 第三方组件集成

对于没有 "use client" 指令的第三方组件,创建一个包装文件:

// app/carousel.tsx
"use client";
import { Carousel } from "acme-carousel";
export default Carousel;

然后在 Server Component 中直接导入这个包装后的组件即可。

7. 路由段配置

// app/api/route.ts
export const dynamic = "force-dynamic"; // 强制动态渲染
export const revalidate = 0; // 禁用缓存
export const runtime = "edge"; // 使用 Edge Runtime
export const maxDuration = 60; // 最大执行时间(秒)

四、要点笔记(康奈尔笔记法)

康奈尔笔记法:将笔记分为线索栏、笔记栏和总结栏,便于复习和检索。

关键概念速查表

线索/关键词详细笔记
App Router基于 /app 目录的路由系统,支持嵌套布局、Server Components
Server Components默认组件类型,服务端渲染,不发送 JS 到客户端,可直接访问数据库
Client Components添加 "use client" 指令,用于交互、状态管理、浏览器 API
Cache ComponentsNext.js 16 新增,"use cache" 指令实现声明式缓存
PPR (Partial Prerendering)默认渲染模式,静态 Shell + 流式动态内容
Server Actions"use server" 定义的服务端函数,可直接在表单中使用
RSC PayloadServer Components 渲染后的二进制格式,用于客户端协调
Streaming通过 <Suspense> 实现流式渲染,先显示 fallback 再加载内容
cacheLife配合 "use cache" 设置缓存存活时间(seconds/minutes/hours 等)
cacheTag为缓存设置标签,配合 revalidateTag / updateTag 按需刷新
文件系统路由文件夹结构即路由,page.js 定义页面,layout.js 定义布局
generateStaticParams为动态路由预生成静态参数,用于 SSG

核心 API 速查

API / 配置用途示例
"use client"声明 Client Component'use client' 加在文件顶部
"use server"声明 Server Action'use server' 加在函数顶部或文件顶部
"use cache"声明缓存(需启用 cacheComponents)'use cache' 加在函数/组件顶部
cacheLife(profile)设置缓存策略cacheLife('hours')
cacheTag(tag)为缓存打标签cacheTag('products')
revalidateTag(tag)按标签刷新缓存revalidateTag('products')
revalidatePath(path)按路径刷新缓存revalidatePath('/blog')
updateTag(tag)立即过期并重新生成缓存updateTag('posts')
cookies()读取请求 cookiesconst store = await cookies()
headers()读取请求 headersconst h = await headers()
redirect(path)重定向redirect('/login')
notFound()触发 404 页面notFound()
<Link href>客户端导航,自动预取<Link href="/about">关于</Link>
<Image>图片优化<Image src="/photo.jpg" alt="..." width={500} height={300} />
<Suspense>流式渲染边界<Suspense fallback={<Loading />}><Component /></Suspense>
generateMetadata动态生成页面 metadataexport async function generateMetadata() { ... }
generateStaticParams预生成动态路由参数export async function generateStaticParams() { ... }
connection()标记需要请求级信息的代码await connection() 后使用 Date.now() 等

本节总结

Next.js 16 是一个基于 React 的全栈框架,核心围绕 App Router 构建应用。默认使用 Server Components 实现零 JS 的服务端渲染,需要交互时通过 "use client" 引入 Client Components。Next.js 16 引入的 Cache Components("use cache")和 Partial Prerendering 极大地简化了缓存策略,将渲染模型统一为"静态 Shell + 流式动态内容"。数据获取直接在 Server Components 中 async/await,无需客户端数据获取库。Server Actions 通过 "use server" 让表单提交和 mutations 回归简单。


五、复习与实践(SQ3R · Recite & Review)

SQ3R 最后两步:复述核心要点,通过实践巩固理解。

核心要点回顾

  1. 文件系统路由:app/ 目录下的文件夹结构就是路由表,page.js 是路由入口,layout.js 是共享布局
  2. Server Components 是默认的:不需要任何指令,组件在服务端渲染,不发送 JS 到客户端
  3. "use client" 是边界:标记了客户端和服务端的分界线,该文件及其所有导入都会打包到客户端
  4. 布局不重新渲染:导航时 layout.js 保持不变,只有 page.js 的内容会更新
  5. 数据获取用 async/await:Server Components 中直接 await fetch 或 ORM 查询,不需要 useEffect
  6. "use cache" + cacheLife:Next.js 16 的声明式缓存方案,替代了之前的 fetch 缓存选项
  7. Streaming 是核心模式:用 <Suspense> 包裹异步内容,先显示 fallback,数据就绪后流式替换
  8. Server Actions 简化 mutations:"use server" 定义服务端函数,直接绑定到表单的 action
  9. PPR 是默认渲染模式:静态内容预渲染,动态内容流式传输,最佳性能组合
  10. 环境安全:非 NEXT_PUBLIC_ 前缀的环境变量不会暴露给客户端,server-only 包提供额外保护

动手练习

练习 1:个人博客(入门级)

创建一个简单的博客应用:

  • 首页展示文章列表
  • 文章详情页使用动态路由 [slug]
  • 添加 loading.tsx 和 error.tsx
  • 使用 generateStaticParams 预生成页面

练习 2:带缓存的商品列表(中级)

构建一个商品展示页:

  • 启用 cacheComponents: true
  • 使用 "use cache" + cacheLife('hours') 缓存商品数据
  • 使用 cacheTag('products') 为缓存打标签
  • 创建 Server Action 实现 updateTag('products') 刷新缓存

练习 3:全栈仪表盘(高级)

创建一个带认证的仪表盘:

  • 使用 cookies() 读取 session
  • 用 <Suspense> 包裹个性化内容,实现流式加载
  • Server Components 获取数据,Client Components 处理图表交互
  • 使用 Context Provider 共享主题状态

常见陷阱

  1. 在 Server Component 中使用 useState 或 onClick —— 这些是客户端 API,必须添加 "use client"。解法:拆分为 Server + Client Components。
  2. 在 Client Component 中直接访问数据库 —— Client Component 在浏览器运行,无法访问服务端资源。解法:在 Server Component 中获取数据,通过 props 传递。
  3. 在 layout 中使用 cookies() 却没有 Suspense 边界 —— Next.js 16 中,运行时 API 访问会导致构建错误。解法:将运行时数据访问提取到子组件中并用 <Suspense> 包裹。
  4. 忘记 await params —— Next.js 16 中 params 是 Promise 类型,必须 await 后才能使用。
  5. 过度使用 "use client" —— 把整个页面标记为 Client Component 会导致大量 JS 发送到客户端。解法:只在最小粒度的交互组件上使用。

延伸阅读