Next.js 16 中的 SSR 與 RSC:生產決策指南
關於 SSR 與 RSC 的討論因炒作、不完整的心理模式以及坦率地說,一些令人困惑的文件而變得混亂。它們不是競爭的技術——它們是在應用程式的不同層解決不同問題的互補工具。但是知道在特定場景中應該使用哪種工具?那是真正的工程判斷所在。
讓我用真實的生產數據、實際的代碼模式以及沒有人在會議演講中談論的權衡,逐步引導你瞭解我學到的一切。
目錄
- 理解基礎知識
- SSR 在 Next.js 16 中如何運作
- React Server Components 如何運作
- 性能比較:真實生產數據
- 套件大小影響
- 串流和瀑布流模式
- 真正有效的快取策略
- 決策框架:何時使用每一個
- 從 Pages Router 遷移模式
- 技術 SEO 含義
- 常見問題

理解基礎知識
在深入細節之前,讓我們建立一個乾淨的心理模式。這比你想的更重要——我見過資深工程師因為術語重疊而混淆 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} />;
}
渲染管道如下所示:
- 請求擊中服務器
- Next.js 自上而下執行 RSC 樹
- 服務器組件解決其非同步操作(數據擷取等)
- 渲染的 RSC 有效負載被序列化
- SSR 將其轉換為初始響應的 HTML
- 客戶端接收 HTML + RSC 有效負載 + 客戶端組件 JS
- React 僅在客戶端組件邊界註水
步驟 3-6 可以通過串流進行,我將在下面詳細介紹。
React Server Components 如何運作
RSC 不僅僅是「在服務器上運行的組件」。它們代表一種根本不同的執行模型。
當服務器組件渲染時,其輸出是 UI 的序列化描述——類似於類似 JSON 的樹結構。此有效負載包括服務器組件的渲染輸出(作為類似 HTML 的節點)和對客戶端組件的引用(作為模塊指針加上其序列化的 props)。
這意味著:
- 服務器組件可以直接訪問資料庫、檔案系統和僅服務器 API
- 它們可以在組件級別使用
async/await - 它們的代碼、依賴項和導入永遠不會出現在客戶端套件中
- 它們無法使用
useState、useEffect或任何瀏覽器 API - 它們無法將函數作為 props 傳遞給客戶端組件(函數不可序列化)
最後一點經常讓人困惑。你不能這樣做:
// ❌ 這將拋出錯誤
async function ServerParent() {
const handleClick = () => console.log('clicked');
return <ClientChild onClick={handleClick} />;
}
你需要將處理程序移到客戶端組件本身中,或使用服務器操作。

性能比較:真實生產數據
我在從 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-fns、marked、sanitize-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)
- 內容可以靜態分析或快取
在以下情況使用客戶端組件:
- 你需要
useState、useEffect、useRef或其他 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 規則和組件邊界約定。