關於 SSR 與 RSC 的討論因炒作、不完整的心理模式以及坦率地說,一些令人困惑的文件而變得混亂。它們不是競爭的技術——它們是在應用程式的不同層解決不同問題的互補工具。但是知道在特定場景中應該使用哪種工具?那是真正的工程判斷所在。

讓我用真實的生產數據、實際的代碼模式以及沒有人在會議演講中談論的權衡,逐步引導你瞭解我學到的一切。

目錄

SSR vs RSC in Next.js 16: A Production Decision Guide

理解基礎知識

在深入細節之前,讓我們建立一個乾淨的心理模式。這比你想的更重要——我見過資深工程師因為術語重疊而混淆 SSR 和 RSC。

服務器端渲染 (SSR) 是一種渲染策略。它決定何時在哪裡將你的組件樹轉換為 HTML。使用 SSR,每個請求都會擊中服務器,將完整的組件樹渲染為 HTML,將其發送到客戶端,然後 React 為整個樹進行註水以使其具有互動性。

React Server Components (RSC) 是一種組件類型。它們決定什麼被發送到客戶端。服務器組件在服務器上執行並將其渲染輸出(作為序列化的 React 樹,而非 HTML)發送到客戶端。它們永遠不會註水。它們的 JavaScript 永遠不會發送到瀏覽器。

看到區別了嗎?SSR 是關於渲染時機。RSC 是關於組件邊界和什麼代碼發送到哪裡。

在具有 App Router 的 Next.js 16.2 中,你實際上同時使用兩者。每個頁面請求都涉及組件樹的服務器端渲染,其中包括服務器組件和客戶端組件。RSC 層決定哪些組件需要註水 JavaScript,SSR 層決定 HTML 如何以及何時生成。

組合模式

這是一個關鍵的洞察,我花了太長時間才內化:在 App Router 中,服務器組件是預設的。你用 'use client' 選擇進入客戶端行為。這翻轉了舊版 Pages Router 模型。

// 這在 App Router 中預設是服務器組件
// 此組件沒有 JavaScript 發送到瀏覽器
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } });
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* 此客戶端組件島獨立註水 */}
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  );
}
// components/AddToCartButton.tsx
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId, price }: Props) {
  const [loading, setLoading] = useState(false);
  // 只有此組件的 JS 發送到瀏覽器
  return <button onClick={handleAdd}>Add to Cart — ${price}</button>;
}

SSR 在 Next.js 16 中如何運作

App Router 中的 SSR 不同於 Pages Router 中的 getServerSideProps。執行模式已從根本上改變。

在 Next.js 16 中,當你設定 dynamic = 'force-dynamic' 或在服務器組件中使用 cookies()headers()searchParams 時,你告訴 Next.js:「此頁面無法靜態生成。在每個請求上新鮮渲染。」

// app/dashboard/page.tsx
import { cookies } from 'next/headers';

export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const session = await cookies();
  const userId = session.get('userId')?.value;
  const data = await fetchDashboardData(userId);
  
  return <DashboardLayout data={data} />;
}

渲染管道如下所示:

  1. 請求擊中服務器
  2. Next.js 自上而下執行 RSC 樹
  3. 服務器組件解決其非同步操作(數據擷取等)
  4. 渲染的 RSC 有效負載被序列化
  5. SSR 將其轉換為初始響應的 HTML
  6. 客戶端接收 HTML + RSC 有效負載 + 客戶端組件 JS
  7. React 僅在客戶端組件邊界註水

步驟 3-6 可以通過串流進行,我將在下面詳細介紹。

React Server Components 如何運作

RSC 不僅僅是「在服務器上運行的組件」。它們代表一種根本不同的執行模型。

當服務器組件渲染時,其輸出是 UI 的序列化描述——類似於類似 JSON 的樹結構。此有效負載包括服務器組件的渲染輸出(作為類似 HTML 的節點)和對客戶端組件的引用(作為模塊指針加上其序列化的 props)。

這意味著:

  • 服務器組件可以直接訪問資料庫、檔案系統和僅服務器 API
  • 它們可以在組件級別使用 async/await
  • 它們的代碼、依賴項和導入永遠不會出現在客戶端套件中
  • 它們無法使用 useStateuseEffect 或任何瀏覽器 API
  • 它們無法將函數作為 props 傳遞給客戶端組件(函數不可序列化)

最後一點經常讓人困惑。你不能這樣做:

// ❌ 這將拋出錯誤
async function ServerParent() {
  const handleClick = () => console.log('clicked');
  return <ClientChild onClick={handleClick} />;
}

你需要將處理程序移到客戶端組件本身中,或使用服務器操作。

SSR vs RSC in Next.js 16: A Production Decision Guide - architecture

性能比較:真實生產數據

我在從 Pages Router(傳統 SSR)遷移到 App Router(RSC + SSR)在 Next.js 16.2 期間對三個生產應用程式進行了受控基準測試。以下是實際數據。

測試環境

  • AWS us-east-1,t3.xlarge 實例
  • PostgreSQL 透過 Prisma,Redis 快取層
  • 透過 Web Vitals RUM 數據測量,時間範圍為 30 天
  • 跨三個應用程式約 230 萬月頁面視圖
指標 Pages Router (SSR) App Router (RSC) 差異
TTFB (p50) 320ms 180ms -43.7%
TTFB (p95) 890ms 410ms -53.9%
FCP (p50) 1.2s 0.8s -33.3%
LCP (p50) 2.1s 1.4s -33.3%
TTI (p50) 3.8s 1.9s -50.0%
INP (p75) 180ms 95ms -47.2%
傳輸的總 JS 387KB 142KB -63.3%
註水時間 (p50) 450ms 120ms -73.3%

TTI 和註水改進是這裡的頭條數據。當你停止為組件樹的 70% 發送組件 JavaScript 時,瀏覽器的工作量會大大減少。

但這裡有一個細微差別:TTFB 改進是因為串流,而不是因為 RSC 本身。App Router 串流 HTML 響應,所以瀏覽器在整個頁面被渲染之前開始接收字節。使用 Pages Router,getServerSideProps 必須在發送任何 HTML 之前完全完成。

套件大小影響

這是 RSC 最閃耀的地方,也是我看到最多誤解的地方。

在傳統的 SSR 設置中,每個組件都將其 JavaScript 發送到客戶端進行註水——即使該組件從不進行任何互動。想想:你的產品描述、你的部落格文章正文、你的頁腳導航。所有這些渲染邏輯都發送到瀏覽器,只是為了讓 React 能夠「註水」它並確認服務器 HTML 相符。

使用 RSC,這些組件根本不發送任何 JavaScript。

對於我們的電子商務客戶之一,以下是套件的細分方式:

組件類別 Pages Router 套件 App Router 套件 節省
佈局/Chrome 45KB 0KB (服務器組件) 100%
產品顯示 38KB 0KB (服務器組件) 100%
導航 22KB 8KB (僅互動部分) 63.6%
搜尋 31KB 28KB (主要是客戶端) 9.7%
購物車/結帳 67KB 62KB (主要是客戶端) 7.5%
第三方庫 184KB 44KB 76.1%
總計 387KB 142KB 63.3%

第三方庫列非常巨大。庫如 date-fnsmarkedsanitize-html——如果它們僅在服務器組件中使用,它們對你的客戶端套件是零成本。我們有一個頁面在服務器組件中使用 sharp 進行影像處理。那是一個 1.2MB 的庫,瀏覽器甚至不知道它。

串流和瀑布流模式

串流是 App Router 的秘密武器,它從根本上改變了你對數據擷取瀑布流的看法。

舊的瀑布流問題

使用 Pages Router SSR:

請求 → getServerSideProps (所有數據) → 渲染 → 發送 HTML → 下載 JS → 註水
         |__________ 800ms ___________|   200ms   |__ 0ms __|__ 300ms __|__ 450ms __|

一切都阻塞在該初始數據擷取上。如果你需要來自三個 API 的數據,它們要麼在 getServerSideProps 中並行運行,要麼你有一個瀑布流。

使用 Suspense 串流

App Router 使用 RSC:

請求 → 渲染殼 → 串流 HTML (即時) → 串流數據部分 → 下載 JS → 註水 (部分)
         |__ 50ms __|    |_____ 0ms _____|       |____ 進行中 ____|   |_ 並行 _|__ 120ms __|

關鍵的區別:瀏覽器立即開始接收 HTML。Suspense 邊界定義頁面的哪些部分在準備好時串流進來。

import { Suspense } from 'react';

export default function ProductPage({ params }) {
  return (
    <div>
      {/* 立即發送 */}
      <Header />
      <ProductHero productId={params.id} />
      
      {/* 在準備好時串流進來 */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>
      
      {/* 獨立串流 */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={params.id} />
      </Suspense>
    </div>
  );
}

每個 Suspense 邊界獨立串流。如果推薦需要 2 秒但評論需要 200 毫秒,評論會首先出現。用戶看到漸進式內容加載,而不是空白屏幕或完整骨架。

避免新的瀑布流

但 RSC 引入了它們自己的瀑布流風險。父子服務器組件數據擷取可以創建順序瀑布流:

// ❌ 順序瀑布流
async function Parent() {
  const user = await getUser(); // 200ms
  return <Child userId={user.id} />; // 在 Parent 解決之前無法開始
}

async function Child({ userId }) {
  const orders = await getOrders(userId); // 300ms
  return <OrderList orders={orders} />;
}
// 總計:500ms

修復是盡可能深地推送數據擷取並使用並行擷取模式:

// ✅ 使用 Suspense 並行
async function Parent() {
  const userPromise = getUser();
  return (
    <>
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile promise={userPromise} />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <UserOrders promise={userPromise} />
      </Suspense>
    </>
  );
}

真正有效的快取策略

Next.js 16 在 14 和 15 版本社區(理所當然地)抱怨複雜性之後改造了快取。這是當前模型的樣子以及 SSR 與 RSC 如何發揮作用。

使用 `fetch` 進行請求級別快取

使用 fetch 的服務器組件可以為每個請求設定快取:

// 快取 60 秒(ISR 行為)
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 60 }
});

// 無快取,每個請求都是新鮮的(SSR 行為)
const data = await fetch('https://api.example.com/user/profile', {
  cache: 'no-store'
});

// 快取帶有標籤以按需重新驗證
const data = await fetch('https://api.example.com/products/123', {
  next: { tags: ['product-123'] }
});

段級別快取

你可以在單個頁面內混合渲染策略:

// 靜態佈局(在構建時快取)
export default function Layout({ children }) {
  return <div><Nav />{children}<Footer /></div>;
}

// 動態頁面(每個請求都是新鮮的)
export const dynamic = 'force-dynamic';
export default async function Page() { /* ... */ }

快取何時變得棘手

真正的問題:如果路由段中的任何組件使用動態函數(cookies()headers()searchParams),則整個段變得動態。深層嵌套服務器組件中的一個未快取擷取使整個頁面變得動態。

這在生產中咬過我們。我們有一個應該是 ISR 快取的產品頁面,但深層嵌套的 RecentlyViewed 組件正在讀取 cookie。整個頁面變得動態,TTFB 從 50 毫秒跳到 400 毫秒,我們兩週都沒注意到。

修復:在 Suspense 邊界後隔離動態組件或將其移到在客戶端擷取的客戶端組件。

決策框架:何時使用每一個

在遷移三個生產應用程式後,這是我使用的決策框架。它不是「SSR 與 RSC」,而更多關於「哪個組件的哪種渲染策略」。

在以下情況使用服務器組件(預設):

  • 組件顯示數據但不需要互動性
  • 你使用僅限服務器的資源(DB、檔案系統、私有 API)
  • 組件導入重型庫(markdown 解析器、語法高亮器)
  • SEO 對內容很重要(搜尋引擎獲得完整 HTML)
  • 內容可以靜態分析或快取

在以下情況使用客戶端組件:

  • 你需要 useStateuseEffectuseRef 或其他 React Hook
  • 你需要瀏覽器 API(localStorage、地理位置、IntersectionObserver)
  • 你需要事件處理程序(onClick、onChange、onSubmit)
  • 你使用需要瀏覽器上下文的第三方庫
  • 你需要實時更新(WebSocket、輪詢)

何時使用 SSR (force-dynamic):

  • 內容因用戶/會話而異
  • 數據變化太頻繁而無法進行 ISR
  • 你需要請求時間資訊(認證狀態、地理位置標頭)
  • SEO 仍需要服務器呈現的 HTML

何時使用靜態生成:

  • 內容不經常變化(行銷頁面、文件、部落格文章)
  • 性能很關鍵(在 CDN 邊緣快取)
  • 內容對所有用戶都相同

對於我們的 Next.js 開發項目,我們通常最終得到大致這樣的分割:60% 服務器組件(靜態)、20% 服務器組件(動態/SSR)、15% 客戶端組件以及 5% 具有 Suspense 邊界的混合模式。

從 Pages Router 遷移模式

如果你正在遷移現有的 Next.js 應用程式,請不要嘗試一次轉換所有內容。我看到這種方式失敗得很慘。以下是有效的漸進方法:

第 1 階段:共存

Next.js 16 同時支援 pages/app/ 目錄。在 app/ 中啟動新路由並保持現有路由不變。

第 2 階段:佈局遷移

先遷移你的佈局。_app.tsx_document.tsx 變成 app/layout.tsx。這通常是最簡單的收獲——佈局是完美的服務器組件。

第 3 階段:首先是靜態頁面

遷移你最簡單的靜態頁面。行銷頁面、關於頁面、部落格文章。這些是直接的服務器組件轉換。

第 4 階段:動態頁面

轉換使用 getServerSideProps 的頁面。這是你會遇到最多摩擦的地方,特別是圍繞數據擷取模式和認證。

第 5 階段:客戶端互動性

將互動島提取到客戶端組件中。這是最困難的部分——你需要識別最小的客戶端邊界。

// 之前:Pages Router 中一切預設都是「客戶端」
// 之後:顯式邊界

// app/products/[id]/page.tsx (服務器組件)
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return (
    <article>
      <h1>{product.name}</h1>
      <ProductGallery images={product.images} /> {/* 客戶端 */}
      <div dangerouslySetInnerHTML={{ __html: product.description }} /> {/* 服務器 */}
      <PricingWidget product={product} /> {/* 客戶端 */}
      <Suspense fallback={<Skeleton />}>
        <RelatedProducts categoryId={product.categoryId} /> {/* 服務器 */}
      </Suspense>
    </article>
  );
}

如果你需要幫助規劃遷移策略,我們的團隊已經做過足夠多次,知道地雷在哪裡——聯繫我們,我們可以討論你的具體架構。

技術 SEO 含義

帶著 12 年以上觀察搜尋引擎如何處理 JavaScript 渲染的經驗,我可以告訴你:RSC 模型是自 SSR 本身以來技術 SEO 最好的事情。

原因如下:

服務器組件在服務器上渲染完整 HTML。 Googlebot 無需執行任何 JavaScript 即可獲得完整內容。這不是新的——SSR 也做了這個。但 RSC 以戲劇性更少的客戶端 JavaScript 進行,直接影響核心網頁活力。

Google 已確認 INP(交互到下一步繪製)自 2024 年 3 月起是排名信號。我們的生產數據顯示 RSC 密集型頁面在 INP 上的得分比等效 SSR 頁面好 47%。更少的 JavaScript = 更少的主線程爭奪 = 更好的 INP。

串流影響爬蟲行為。 Googlebot 自 2023 年起支援 HTTP 串流,但它有超時。如果你最慢的 Suspense 邊界需要 15 秒,Googlebot 可能不會等待。將關鍵 SEO 內容保留在 Suspense 邊界之外,或確保你的 suspense 後退內容包含有意義的內容。

對於 SEO 是主要考慮因素的客戶端,我們通常推薦我們的 無頭 CMS 開發方法與 App Router 配對——內容位於 CMS、透過服務器組件渲染,並向瀏覽器發送零不必要的 JavaScript。這是搜尋性能的最好的一切。

Astro 也值得考慮,如果你的網站主要是內容驅動的,互動很少。但對於具有豐富互動功能的應用程式,Next.js 16 與 RSC 達到了最佳點。

常見問題

Next.js 16 中 SSR 和 RSC 之間有什麼區別? SSR(服務器端渲染)是一種渲染策略,決定你的頁面 HTML 何時生成——在每個請求、服務器上。React Server Components (RSC) 是一種組件類型,決定什麼代碼發送到瀏覽器。在 App Router 中,它們一起工作:RSC 定義什麼需要客戶端 JavaScript,SSR 處理 HTML 生成。你通常同時使用兩者。

React Server Components 是否取代了服務器端渲染? 不是。RSC 和 SSR 是互補的,而不是競爭的。在 Next.js 16 的 App Router 中,每個頁面都使用 SSR 來獲得初始 HTML 響應。RSC 決定該頁面中哪些組件需要發送 JavaScript 到客戶端以進行註水。你可以有一個完全 SSR 的頁面,由完全由服務器組件組成(無客戶端 JS)或兩者的混合。

React Server Components 減少了多少套件大小? 在我們的生產測量中,基於 RSC 的 App Router 頁面與等效的 Pages Router 實現相比,JavaScript 套件平均小 63%。節省量主要取決於你的組件樹——具有大量僅顯示內容的頁面看到最大收益,而高度互動的頁面(儀表板、編輯器)看到較小的改進。

我應該將現有的 Next.js 應用程式遷移到 App Router 嗎? 這取決於你的痛點。如果你的核心網頁活力因龐大的 JavaScript 套件而受損,或者你的 TTFB 因順序數據擷取而很高,遷移值得。如果你的 Pages Router 應用程式性能良好且你的團隊富有成效,就沒有緊急性。Next.js 同時支援兩個路由,所以你可以逐步遷移。

快取如何與 Next.js 16 中的服務器組件配合使用? Next.js 16 大大簡化了快取模型。服務器組件可以靜態快取(靜態數據的預設值)、在時間基礎上重新驗證(ISR)或在每個請求時新鮮渲染(動態)。你通過 next: { revalidate } 在擷取級別或通過 export const dynamic 在路由段級別控制此。小心:段中的一個動態函數使整個段變得動態。

服務器組件是否影響 SEO? 服務器組件對 SEO 非常出色。它們在服務器上渲染完整 HTML,搜尋引擎可以在不執行 JavaScript 的情況下建立索引。此外,減少的客戶端 JavaScript 改進核心網頁活力分數,特別是 INP 和 TTI,這是排名信號。一個警告是 Suspense 邊界內的內容逐步串流,所以確保關鍵 SEO 內容不在緩慢數據擷取後面。

我可以將 React Server Components 與無頭 CMS 一起使用嗎? 絕對可以——這是最好的配對之一。服務器組件可以直接在組件級別擷取 CMS 內容,而無需向客戶端公開 API 金鑰或 CMS SDK 代碼。Contentful SDK、Sanity 客戶端或 Prismic 的 @prismicio/client 等庫完全保留在服務器上。結合 ISR 或透過 webhook 按需重新驗證,你可以獲得快速、可快取的頁面,零不必要的客戶端 JavaScript。

在生產中使用 RSC 時最大的陷阱是什麼? 我遇到的三個最大問題:(1) 嵌套服務器組件中意外的瀑布流數據擷取——使用 React DevTools 和服務器計時標頭進行資料紀錄和修復。(2) 通過在嵌套組件中使用 cookies()headers() 意外使快取的頁面變成動態。(3) 從服務器組件傳遞不可序列化數據(函數、類實例、日期)到客戶端組件時出現 Props 序列化錯誤。及早建立良好的 linting 規則和組件邊界約定。