5 min read

Next.js 完全指南:渲染模式与实战技巧

Table of Contents

引言

Next.js 作为 React 生態中最成熟的全端框架,提供了多种渲染策略来满足不同场景的需求。但正因为选择太多,很多开发者在实际项目中常常困惑:什么时候用 SSR?什么时候用 SSG?ISR 又适合什么场景?这篇文章将带你深入理解这些渲染模式,并分享真实项目中的踩坑经验。


Next.js 渲染模式全解析

1. 静态站点生成 (SSG)

原理:在构建时生成 HTML,每次请求复用相同的页面。

适用场景

  • 博客、文档网站
  • 产品介绍页
  • 不频繁更新的内容页面
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}

优点

  • 极速加载(CDN 缓存)
  • SEO 友好
  • 降低服务器成本

缺点

  • 构建时确定,适合静态内容
  • 更新需要重新部署

2. 服务器组件 (RSC)

原理:组件在服务器端渲染,默认不发送 JavaScript 到客户端。

适用场景

  • 需要直接访问数据库或文件系统的组件
  • 大型数据获取逻辑
  • 减少客户端 bundle 大小
// app/dashboard/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store', // 动态获取
  });
  return res.json();
}

export default async function Dashboard() {
  const data = await getData();
  return <div>{data.content}</div>;
}

优点

  • 零客户端 JavaScript
  • 直接访问后端资源
  • 自动代码分割

缺点

  • 交互组件需要转为客户端组件
  • 学习曲线(需要理解服务器/客户端边界)

3. 动态渲染 (SSR)

原理:每次请求时实时渲染页面。

适用场景

  • 个性化内容(用户专属页面)
  • 实时数据展示
  • 需要请求参数的页面
// app/user-profile/page.tsx
import { headers } from 'next/headers';

export default async function ProfilePage() {
  const headersList = headers();
  const cookie = headersList.get('cookie');
  
  // 每次请求都获取最新数据
  const user = await getUserFromCookie(cookie);
  
  return <div>Welcome, {user.name}!</div>;
}

优点

  • 实时数据
  • SEO 友好
  • 个性化内容

缺点

  • TTFB(首字节时间)较慢
  • 高并发时服务器压力大

4. 增量静态再生成 (ISR)

原理:构建时生成静态页面,后台定期重新验证更新。

适用场景

  • 频繁更新的内容(博客、新闻)
  • 电商产品页
  • 需要 SEO 且内容会更新的页面
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}</p>
    </div>
  );
}

// 每60秒重新验证
export const revalidate = 60;

优点

  • 静态速度 + 动态更新
  • 无需重新部署
  • 适合大多数内容型网站

缺点

  • 更新有延迟(最大一个 revalidate 周期)
  • 需要理解缓存机制

渲染模式选择决策树

├── 数据是否需要实时?
│   ├── 是 → SSR (动态渲染)
│   └── 否 → 下一题
├── 内容更新频率?
│   ├── 极高(每分钟) → SSR
│   ├── 较高(每天) → ISR
│   └── 很低(几个月) → SSG
└── 是否需要用户个性化?
    ├── 是 → SSR 或客户端获取
    └── 否 → SSG / ISR

实战踩坑与解决方案

踩坑 1:客户端组件 vs 服务器组件边界混淆

问题:在服务器组件中直接使用 useState 或 onClick,导致报错。

// ❌ 错误:服务器组件中使用交互 API
export default function Page() {
  const [count, setCount] = useState(0); // 报错!
  
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

解决:使用 'use client' 声明客户端组件,或将交互逻辑拆分。

// ✅ 方案一:拆分为客户端组件
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// ✅ 方案二:服务器组件包裹客户端组件
export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <Counter />
    </div>
  );
}

踩坑 2:fetch 缓存行为理解错误

问题:以为设置了 revalidate 就会实时更新,但实际上默认行为不同。

// ❌ 错误:同时使用 cache 和 revalidate
const data = await fetch('/api/data', {
  cache: 'no-store', // 这会忽略 revalidate
  revalidate: 3600,
});

解决:根据需求选择正确的缓存策略。

场景cacherevalidate
静态内容’force-cache’设置数值
实时数据’no-store’不设置
ISR’force-cache’设置数值
// ✅ 正确:ISR 模式
const data = await fetch('/api/data', {
  next: { revalidate: 60 }, // 每60秒重新生成
});

// ✅ 正确:SSR 模式
const data = await fetch('/api/data', {
  cache: 'no-store',
});

踩坑 3:Next.js Image 组件使用不当

问题:图片加载出现布局偏移(CLS),或图片不显示。

// ❌ 错误:未设置宽高
<Image src={post.coverImage} alt={post.title} />

解决:始终为已知尺寸的图片设置宽高,或使用 fill 配合父容器。

// ✅ 方案一:已知尺寸
<Image 
  src={post.coverImage} 
  alt={post.title}
  width={800}
  height={400}
/>

// ✅ 方案二:响应式(父容器需要 position: relative)
<div className="relative w-full h-64">
  <Image 
    src={post.coverImage} 
    alt={post.title}
    fill
    className="object-cover"
  />
</div>

踩坑 4:环境变量使用不当

问题:客户端代码中使用了 .env 中的变量,导致泄露或 undefined。

解决:只有以 NEXT_PUBLIC_ 开头的变量才会暴露到客户端。

# .env.local
# ❌ 错误:私钥暴露(不会报错,但客户端无法访问)
DATABASE_URL=postgres://...

# ✅ 正确:客户端可见需要前缀
NEXT_PUBLIC_API_URL=https://api.example.com
// ✅ 服务器组件中,两种都能用
export default async function Page() {
  const secret = process.env.DATABASE_URL; // ✓ 可用
  const publicUrl = process.env.NEXT_PUBLIC_API_URL; // ✓ 可用
  return <div>{publicUrl}</div>;
}

// ✅ 客户端组件中,只有 NEXT_PUBLIC_ 可用
'use client';
export function ClientComponent() {
  const url = process.env.NEXT_PUBLIC_API_URL; // ✓ 可用
  // const secret = process.env.DATABASE_URL; // ✗ undefined
  return <div>{url}</div>;
}

踩坑 5:动态路由参数类型问题

问题:在 Next.js 15+ 中,params 变成了 Promise,需要 await。

// ❌ 错误:Next.js 15+ 直接使用 params
export default function Page({ params }) {
  const { slug } = params;
  // 可能是 undefined 或 Promise
}

// ✅ 正确:Next.js 15+ 需要 await
export default async function Page({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.title}</article>;
}

性能优化实战技巧

1. 使用 next/script 优化第三方脚本

import Script from 'next/script';

export default function Page() {
  return (
    <>
      <Script 
        src="https://analytics.example.com/script.js"
        strategy="lazyOnload" // 空闲时加载
      />
    </>
  );
}

strategy 选项

  • beforeInteractive:最优先(关键脚本)
  • afterInteractive:默认(大多数场景)
  • lazyOnload:空闲时加载(非关键)

2. 路由预加载

import Link from 'next/link';
import { prefetch } from 'next/navigation';

// 方法一:Link 组件自动预加载
<Link href="/about">About</Link>

// 方法二:手动预加载
<Link 
  href="/products/[id]"
  as={`/products/${product.id}`}
  prefetch={true} // 默认 true
>
  {product.name}
</Link>

3. 流式渲染 (Streaming)

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Loading />}>
        <HeavyComponent />
      </Suspense>
      <Footer />
    </div>
  );
}

总结

Next.js 的渲染模式选择:

模式构建时请求时适用场景
SSG静态内容
ISR频繁更新的静态内容
SSR个性化/实时数据
RSC混合场景

记住:没有最好的模式,只有最适合的模式。根据你的具体场景选择合适的渲染策略,结合本文的踩坑经验,相信你能在 Next.js 项目中少走很多弯路。

Happy coding! 🚀