返回首页
🎨 前端 / Web

Next.js 16 App Router 最佳实践:10 条核心规则

Next.js 16 + App Router 的最佳实践清单。本文提炼 10 条核心规则,帮你避免常见坑,让项目结构更优雅。

Next.js · App Router · React · 前端 · RSC
📰

今日技术简讯

📰 技术简讯 · 2026-06-13

今日聚合 7 条热门技术内容。

🤖 AI / LLM

1. Anthropic 发布 Prompt Caching

2. Groq 推出 Llama 4 推理服务

🎨 前端 / Web

3. Next.js 16 App Router 最佳实践

4. React Router 7 正式发布

  • 链接https://reactrouter.com
  • 来源:Remix 团队
  • 摘要:React Router 7 整合 Remix 特性,单页应用 + SSR 双模式。

⚙️ 后端 / 架构

5. Apache Kafka 4.0 发布

🚀 独立开发 / OPC

6. Framer Motion 改名 Motion

  • 链接https://motion.dev
  • 来源:Motion 团队
  • 摘要:Framer Motion 改名为 Motion,独立产品定位更清晰。

7. Paddle 推出 Billing 2.0


数据来源:HN / Reddit / 各厂博客 采集时间:2026-06-13 09:00 (UTC+8)

📝

今日深度文

Next.js 16 App Router 最佳实践:10 条核心规则

一句话结论:App Router 不只是"另一种路由方式",而是新的架构范式。遵循这 10 条规则,项目质量和性能会显著提升。

背景

Next.js 16 的 App Router 已经成熟,但很多团队仍在用 Pages Router 的思维写 App Router。结果:

  • 性能没提升(仍在 hydrate 整个页面)
  • JS bundle 仍很大
  • SEO 仍不友好

下面 10 条规则是从 Vercel 团队和大型项目实战中提炼的核心原则。

规则 1:默认 Server Component,加 'use client' 才用 Client

// ❌ 反面:默认全部 'use client'
'use client';

export default function Page() {
  const [count, setCount] = useState(0);
  return <div onClick={() => setCount(count + 1)}>{count}</div>;
}

// ✅ 正面:默认 Server,只交互部分 Client
// app/page.tsx (Server)
export default async function Page() {
  const data = await getData();
  return (
    <div>
      <h1>{data.title}</h1>
      {/* 只有这个按钮需要交互 */}
      <LikeButton id={data.id} />
    </div>
  );
}

// components/LikeButton.tsx (Client)
'use client';
export function LikeButton({ id }: { id: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}

效果:JS bundle 从 200KB 降到 5KB。

规则 2:数据获取放在 Server Component,不用 useEffect

// ❌ 反面:客户端获取
'use client';

export default function Products() {
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts);
  }, []);
  
  return <ProductList products={products} />;
}

// ✅ 正面:服务端直接获取
// app/products/page.tsx (Server)
export default async function Products() {
  const products = await db.products.findMany();
  return <ProductList products={products} />;
}

优势

  • 零 loading 状态
  • 更安全(数据库不暴露给客户端)
  • 更高效(避免二次请求)

规则 3:用 Promise.all 并行获取数据

// ❌ 反面:串行 await(瀑布流)
const user = await getUser();
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);

// ✅ 正面:并行获取
const [user, posts, comments] = await Promise.all([
  getUser(),
  getPosts(),
  getComments(),
]);

效果:首屏时间从 2.5s 降到 0.8s。

规则 4:用 Suspense 流式渲染慢的部分

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<ChartSkeleton />}>
        <SlowChart />  {/* 即便慢也不阻塞其他 */}
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
    </div>
  );
}

async function SlowChart() {
  // 模拟慢查询
  const data = await fetch('/api/heavy-stats', { 
    next: { revalidate: 60 } 
  });
  return <Chart data={await data.json()} />;
}

效果:用户立即看到骨架屏 → 渐进填充,无需 loading spinner。

规则 5:路由组 (Route Groups) 组织代码,不用嵌套 URL

// 文件结构:
// app/
//   (marketing)/
//     page.tsx              → /
//     pricing/page.tsx      → /pricing
//   (app)/
//     dashboard/page.tsx    → /dashboard
//     settings/page.tsx     → /settings
//   layout.tsx              → 根布局

好处

  • 用 layout 区分"营销页"和"应用页"(不同布局)
  • URL 保持简洁(不暴露 (marketing) / (app)
  • 代码组织更清晰
// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
  return (
    <div>
      <Header />
      <main className="container mx-auto">{children}</main>
      <Footer />
    </div>
  );
}

// app/(app)/layout.tsx
export default function AppLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  );
}

规则 6:用 Server Actions 替代 API routes(mutation 场景)

// ❌ 反面:写 API route + Client Component
// app/api/posts/route.ts
export async function POST(req: NextRequest) {
  const data = await req.json();
  await db.posts.create({ data });
  return NextResponse.json({ success: true });
}

// components/NewPostForm.tsx
'use client';
export function NewPostForm() {
  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      await fetch('/api/posts', { method: 'POST', body: ... });
    }}>
      ...
    </form>
  );
}

// ✅ 正面:Server Actions
// app/posts/actions.ts
'use server';
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  await db.posts.create({ data: { title } });
  revalidatePath('/posts');
}

// components/NewPostForm.tsx
export function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">发布</button>
    </form>
  );
}

优势:少写 50% 代码,自动处理错误、重试、CSRF。

规则 7:用 generateMetadata 做动态 SEO

// app/products/[id]/page.tsx
import { Metadata } from 'next';

export async function generateMetadata({ 
  params 
}: { 
  params: { id: string } 
}): Promise<Metadata> {
  const product = await db.products.findUnique({ 
    where: { id: params.id } 
  });
  
  return {
    title: `${product.name} | 我的商店`,
    description: product.description,
    openGraph: {
      images: [product.image],
    },
  };
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await db.products.findUnique({ 
    where: { id: params.id } 
  });
  
  return <ProductDetail product={product} />;
}

优势:每个产品页都有独特的 SEO meta,无需 client-side。

规则 8:用 generateStaticParams 预渲染高频路由

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await db.posts.findMany({
    where: { published: true },
    select: { slug: true },
    take: 100, // 预渲染最热的 100 篇
  });
  
  return posts.map(post => ({ slug: post.slug }));
}

export default async function PostPage({ params }) {
  const post = await db.posts.findUnique({ 
    where: { slug: params.slug } 
  });
  return <Article post={post} />;
}

效果:热门文章首屏 0ms(直接 HTML),长尾文章 ISR(按需渲染)。

规则 9:用 Parallel Routes + Intercepting Routes 做模态

// 文件结构:
// app/
//   @modal/
//     (.)photos/[id]/page.tsx  → 拦截 /photos/[id],作为模态
//   photos/
//     [id]/page.tsx           → /photos/[id] 正常页面
//   layout.tsx                 → 同时渲染 page 和 modal
// app/layout.tsx
export default function Layout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <>
      {children}
      {modal}
    </>
  );
}

效果:列表 → 点击照片 → 弹出模态(URL 仍是 /photos/123),刷新 = 完整页。

规则 10:用 revalidate 做 ISR,不用全量重建

// app/posts/page.tsx
export const revalidate = 60; // 每 60 秒重新验证

export default async function Posts() {
  const posts = await db.posts.findMany();
  return <PostList posts={posts} />;
}
// 按需重新验证
// app/posts/actions.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function publishPost(id: string) {
  await db.posts.update({
    where: { id },
    data: { published: true },
  });
  revalidatePath('/posts'); // 立即失效缓存
  revalidatePath(`/posts/${id}`);
}

优势:内容更新即时生效,无需重新部署。

5 个常见错误

错误 1:在 Server Component 里用 hooks

// ❌ 报错
export default async function Page() {
  const [count, setCount] = useState(0); // ❌
}

错误 2:Client Component 嵌套 Server Component

'use client';
import { ServerComponent } from './server'; // ❌

错误 3:把 'use client' 放在文件顶部,但实际只在小组件用

// ❌ 反面:整个页面 'use client'
'use client';

export default function Page() {
  return (
    <div>
      <Header />  {/* 这部分本可 Server */}
      <InteractiveForm />  {/* 只有这个需要 Client */}
    </div>
  );
}

// ✅ 正面:把 'use client' 下沉到交互组件
// app/page.tsx (Server)
export default function Page() {
  return (
    <div>
      <Header />
      <InteractiveForm />  {/* 这个文件自己 'use client' */}
    </div>
  );
}

错误 4:用 useEffect 获取数据

'use client';

export default function Page() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api/data').then(...); // ❌ 应该有 Server Component
  }, []);
}

错误 5:忘记 add 'use server' to action

// app/actions.ts
export async function deleteUser(id: string) {  // ❌ 缺 'use server'
  await db.users.delete({ where: { id } });
}

性能优化清单

- [ ] 所有数据获取在 Server Component
- [ ] 默认无 'use client'
- [ ] 用 Promise.all 并行获取
- [ ] 用 Suspense 流式渲染
- [ ] 静态生成用 generateStaticParams
- [ ] ISR 用 revalidate 配置
- [ ] 图片用 next/image
- [ ] 字体用 next/font
- [ ] 第三方脚本用 next/script (策略:lazyOnload)
- [ ] 监控 Core Web Vitals

我的看法

App Router 是 Next.js 的未来,也是 RSC 落地最成熟的框架:

  1. 学习曲线:需要重新理解"边界"概念,但回报巨大
  2. 生态:所有 Next.js 插件都已支持
  3. Vercel 投入:未来 App Router 是主推方向

我的建议

  • 新项目:直接用 App Router
  • 旧项目:渐进式迁移,先把"只读"页面转 RSC
  • 状态复杂的页面:保留 Pages Router,不必强转

参考


本文规则基于 Vercel 官方推荐 + 多个生产项目实战,2026 年 6 月最新整理。

📚 同主题文章

🎨 前端 / Web 分类更多