一、四种渲染模式的架构对比

Next.js 13+的App Router引入了全新的渲染体系,支持在路由级别甚至组件级别自由组合渲染策略。理解各模式的数据流差异,是做出正确技术选型的前提。

1.1 四种渲染模式对比

// Next.js 13 App Router 渲染模式
//
// ① Static Generation (SSG)
// 渲染时机:Build时
// 缓存:CDN(永不失效,除非手动revalidate)
// 适用:博客、文档、Marketing页面
// 特点:最快,无服务端开销
//
// ② ISR (Incremental Static Regeneration)
// 渲染时机:首次请求时 + 后台定时
// 缓存:CDN(按revalidate时间失效)
// 适用:频繁更新但有缓存需求的页面
// 特点:静态速度 + 动态更新
//
// ③ SSR (Server-Side Rendering)
// 渲染时机:每个请求时
// 缓存:no-cache(每次服务端渲染)
// 适用:需要实时数据、用户相关数据
// 特点:最新数据,较高服务端开销
//
// ④ PPR (Partial Prerendering)
// 渲染时机:Build时静态骨架 + 请求时动态
// 适用:静态内容+动态个性化混合
// 特点:Streaming + Suspense组合

// 性能对比(1000次请求测试环境):
// 模式          TTFB(中位数)   P99延迟
// SSG           28ms           45ms    ← 最快
// ISR(60s)      28ms→620ms    45ms→800ms  (缓存过期瞬间)
// SSR           180ms          450ms
// PPR           30ms           65ms

二、App Router的缓存策略

2.1 fetch请求的缓存层级

// Next.js 13 App Router的fetch缓存配置
// 缓存粒度细化到每个fetch请求

// app/blog/[slug]/page.tsx

// 缓存策略一:force-cache(默认,SSG)
async function getBlogPost(slug: string) {
  const res = await fetch('https://api.example.com/posts/' + slug, {
    cache: 'force-cache'  // 默认值,可省略
  });
  return res.json();
}
// → Build时预渲染,结果永久缓存(等效SSG)

// 缓存策略二:no-store(SSR,每次请求都重新获取)
async function getUserProfile(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    cache: 'no-store'     // 关闭缓存,每次SSR
  });
  return res.json();
}
// → 每次请求执行服务端渲染,数据最新

// 缓存策略三:revalidate(ISR,N秒后自动失效)
async function getNews() {
  const res = await fetch('https://news.api/latest', {
    next: { revalidate: 60 }  // 60秒内使用缓存
  });
  return res.json();
}
// → 60秒内:返回缓存(CDN)
// → 60秒后:第一个请求触发revalidate,后台重新获取

// 缓存策略四:标签级失效(精确控制)
async function getProduct(id: string) {
  const res = await fetch(`https://shop.api/products/${id}`, {
    next: { tags: ['product', `product-${id}`] }
  });
  return res.json();
}
// 调用 revalidateTag('product-123') → 只清除特定标签的缓存

2.2 动态路由与generateStaticParams

// 动态路由的预渲染策略
// app/products/[category]/[slug]/page.tsx

// 方式一:全量预渲染(SSG,适合小规模)
// build时生成所有产品页
export async function generateStaticParams() {
  const products = await fetchAllProducts(); // 获取全量数据
  return products.map(p => ({
    category: p.category,
    slug: p.slug
  }));
}
// 优点:所有页面预渲染,TTFB极低
// 缺点:产品数量大时Build时间爆炸(10万SKU = 10万次fetch)

// 方式二:部分预渲染 + fallback
// 只预渲染热门产品,其他动态生成
export async function generateStaticParams() {
  const hotProducts = await fetchHotProducts(); // 只取热门1000个
  return hotProducts.map(p => ({
    category: p.category,
    slug: p.slug
  }));
}

// fallback: 'blocking'(默认)
// 非预渲染页面:首次请求时服务端渲染,之后CDN缓存
// 访问 /products/electronics/xyz-123(非热门)
// → SSR → 渲染后缓存 → 后续请求走缓存

// fallback: false
// → 未预渲染的页面直接返回404

// 方式三:ISR + 动态混合(推荐大规模电商)
export const revalidate = 3600; // 每小时重新验证

export async function generateStaticParams() {
  // 只预渲染一级分类页
  const categories = await fetchCategories();
  return categories.map(c => ({ category: c.slug }));
}

// 产品详情页(非预渲染,动态生成)
export default async function ProductPage({ params }) {
  // 使用revalidate实现"准实时"缓存
  const product = await getProduct(params.slug);
  return <ProductDetail product={product} />;
}

三、Streaming SSR与Suspense的结合

3.1 流式渲染的原理

// Streaming SSR = 分块传输 + Suspense边界
//
// 传统SSR:全部数据获取 → 完整HTML → 一次性返回
// Streaming SSR:先返回骨架 → 数据就绪后逐块推送
//
// 传输流(HTTP Chunked Transfer Encoding):
// HTTP/1.1 200 OK
// Content-Type: text/html
// Transfer-Encoding: chunked
//
// ① 先发送静态HTML骨架(立即)
// 
//   
//   
//
← Suspense fallback //
// // ② 然后发送动态内容(数据就绪后) // // app/dashboard/page.tsx import { Suspense } from 'react'; // 慢数据组件(独立Suspense边界) async function ProductList() { const products = await fetchProductsSlow(); // 模拟慢查询 2s return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>; } async function RecentOrders() { const orders = await fetchRecentOrders(); // 快,200ms return <div>{orders.length} recent orders</div>; } // 页面骨架立即返回,动态区域逐步填充 export default function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* 快速组件:先渲染 */} <Suspense fallback={<div>Loading orders...</div>}> <RecentOrders /> </Suspense> {/* 慢速组件:骨架屏 → 数据填充 */} <Suspense fallback={<ProductListSkeleton />}> <ProductList /> </Suspense> </div> ); }

3.2 Streaming vs 非Streaming的时序差异

// 时序对比(Dashboard页面,有快慢两个数据源)
//
// 传统SSR时序:
// t=0ms    → 收到请求
// t=0-200ms → 获取RecentOrders (200ms)
// t=200-2200ms → 获取ProductList (2000ms)
// t=2200ms → HTML完整,发送到客户端
// t=2200ms+ → First Contentful Paint
// 结论:用户等了2.2秒才看到任何内容

// Streaming SSR时序:
// t=0ms    → 收到请求
// t=0ms    → 发送HTML骨架(立即)
// t=200ms  → RecentOrders数据就绪,flush更新
// t=200ms  → FCP(用户看到骨架+快速内容)
// t=2000ms → ProductList数据就绪,flush更新
// t=2000ms → 完整内容
// 结论:200ms时用户就看到内容,不用等待2秒

// 实际测试(真实网络):
// TTFB(非Streaming):2200ms → FCP 2200ms
// TTFB(Streaming):  200ms  → FCP 200ms
// TTFB差距:10倍
// 感知体验:骨架屏 + 逐步填充 vs 长时间白屏

四、RSC(React Server Components)架构

4.1 Server Components vs Client Components

// App Router的组件树:
// Server Components(默认,不带'use client')
//   ├── 直接访问数据库/文件系统
//   ├── 不参与hydration(零JS发送到客户端)
//   ├── 可以使用async/await
//   └── 可以<ClientComponent>包装客户端组件
//
// Client Components(带'use client'指令)
//   ├── 参与React hydration
//   ├── 可以使用useState、useEffect、事件处理
//   └── 不能直接访问服务端资源

// app/blog/page.tsx(Server Component,默认)
import { db } from '@/lib/db';

export default async function BlogIndex() {
  // 直接在服务端访问数据库,不经过API层
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  });

  // posts只在服务端存在,不会发送到浏览器
  return (
    <main>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      {/* LikeButton是Client Component,嵌入Server Component */}
      <LikeButton postId={posts[0].id} initialCount={0} />
    </main>
  );
}

// components/LikeButton.tsx(Client Component)
'use client';  // 必须声明'use client'

import { useState } from 'react';

export function LikeButton({ postId, initialCount }) {
  const [count, setCount] = useState(initialCount);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      ❤️ {count}
    </button>
  );
}

4.2 水合(Hydration)优化

// 传统SSR + Hydration的问题:
// 服务端渲染HTML → 浏览器下载JS → 执行JS → 绑定事件
// "两遍渲染":服务端一遍,客户端一遍

// Next.js的优化:减少Hydration体积
//
// app/layout.tsx(根布局是Server Component)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

// ❌ 误区:在根布局使用useState
// 'use client'
// export default function RootLayout({ children }) {
//   const [theme, setTheme] = useState('dark'); // ❌ 无法工作
// }
// 根布局是Server Component,不能使用hook

// ✅ 正确做法:创建ThemeProvider包装
// app/providers.tsx
'use client';
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// app/layout.tsx
import { ThemeProvider } from './providers';
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}