React Server Component(RSC)是 React 新一代核心能力,透過在伺服器端渲染元件、串流傳輸與減少 bundle size,大幅提升 Next.js 應用效能與使用者體驗,並強化資料安全與開發流程。
Written by: Chia1104 CC BY-NC-SA 4.0
隨著前端應用程式的複雜性不斷增加,React 生態系統也在不斷演進以滿足開發者和使用者的需求。React Server Components (RSC) 是 React 團隊推出的一項創新技術,旨在改善應用程式的效能和開發者體驗。RSC 允許開發者將部分元件的渲染過程移至伺服器端,從而優化客戶端的性能和資源利用。隨著 Next.js 15 在 2024 年正式將 RSC 設為預設架構,2025 年將成為 RSC 成為 React 生態系統標準原語的關鍵一年。
伺服器端渲染組件:RSC 允許部分元件僅在伺服器端渲染,這些元件無需在客戶端生成,從而減少了客戶端的負擔。這些組件在伺服器上執行後,會以序列化的 React 樹形式傳送到客戶端。
無狀態且可串流(streaming):RSC 組件通常是無狀態的,意味著它們不依賴於客戶端的狀態管理,並且可以進行串流傳輸以提升渲染速度。透過串流機制,客戶端無需等待所有資料完成即可開始後續的渲染處理。
提升效能:透過將重的計算或資料獲取方式移至伺服器,RSC 減少了客戶端的運算需求,從而提升應用程式的整體效能。
在這個模式下的元件大概可以分成以下三種:
'use client' 指令來明確標記。在這個模式下 RSC 跟 Client Component 是可以共存的,並使用混合渲染的方式。
這裡稍微提醒一件事,RSC 只做一件單一的事情就是在伺服器端生成 React 元件,而畫面渲染則是跟著 React 的 render 方法走的,有可能是在 server side 跟著 client component 一起生成完整的 HTML,或在 client side 跟著 client component 做元件更新的互動。(How are Server Components rendered?)
RSC 在伺服器端渲染完成後,會以序列化的 RSC Payload 格式(content-type 為 text/x-component)透過 streaming 傳送到客戶端。這個 RSC Payload 包含了:
透過串流機制,客戶端的 React 不需要等待所有資料完成,就可以開始處理和顯示內容。
1:"$Sreact.fragment"
2:I[66334,[],""]
3:I[56948,[],""]
6:I[25203,["1029","static/chunks/1029-c0bfd2a11c61893b.js","3599","static/chunks/3599-ccb90f71afb27819.js","3008","static/chunks/3008-fc47ddb25fe42f17.js","4418","static/chunks/4418-abf5f06038322eb3.js","1069","static/chunks/1069-67d4d50c882438ea.js","139","static/chunks/139-d532539c2c23eacb.js","4719","static/chunks/app/%5Blocale%5D/(blog)/%5Btype%5D/error-08e3b20720a5ffdf.js"],"default"]
8:I[99526,[],"OutletBoundary"]關鍵概念:Server Components 不會 Hydrate
需要特別注意的是:Server Components 永遠不會在客戶端 hydrate。只有 Client Components 才需要 hydration 來附加事件監聽器和實現互動性。
| 元件類型 | 伺服器端行為 | 客戶端行為 | Hydration |
|---|---|---|---|
| Server Components | 渲染並序列化為 RSC Payload | 不執行、不下載 JS | 不 hydrate |
| Client Components | 預先渲染成 HTML | 下載 JS、執行 | 需要 hydrate |
最後整個 Component 的結構會類似這樣,而還在 streaming 的元件(promise 尚未 resolved)的時候可以透過 React.Suspense 來觸發 fallback 的 <ClientLoading />。Server Components 的資料擷取 API 與 Suspense 深度整合,可提供載入狀態並在串流完成前解鎖部分內容。
const App = () => {
return (
<ClientLayout>
<ServerAuthGuard> {/* <-- RSC */}
<ClientHeader />
<React.Suspense fallback={<ClientLoading />}>
<ServerContent /> {/* <-- RSC */}
</React.Suspense>
<ClientFooter />
</ServerAuthGuard> {/* <-- RSC */}
</ClientLayout>
);
};由於 RSC 在伺服器端生成,最後傳給客戶端的已經是渲染過的元件結果,Server Components 的程式碼和依賴完全不會傳送到客戶端。如同這個範例若是這元件在客戶端生成,那就會因為 papaparse 而增加 263 kB,然而我們在伺服器端生成的話,就不需要在意這 library 的 bundle size 了。
import Papa from "papaparse";
export default async function Analytics() {
const csv = await getAnalyticsCSV();
const parsed = Papa.parse(csv);
return (
<table>
{/* parsed data render */}
</table>
)
}這種效能提升在實際應用中帶來了顯著的使用者體驗改善。Server Components 允許你將大部分資料擷取移至伺服器端,使客戶端不必發出那麼多請求,同時消除了客戶端上典型的 useEffect 網路瀑布流。
過去客戶端只能透過 fetch 來取得 server 資訊,然而 RSC 可以直接存取資料庫的資料,用來減少客戶端的載入時間。這種能力讓開發者可以在元件層級直接進行資料庫查詢,無需建立額外的 API 端點。
export default function Users() {
const users = await sql`SELECT * FROM users`
return (
<table>
{/* users data render */}
</table>
)
}Server Components 可以定義為非同步函數,讓你可以在元件渲染階段直接 await 資料擷取操作。這種模式大幅簡化了資料流程,並提供更好的開發者體驗。
過去在存取部分第三方服務時所攜帶的 API Key,可能需要透過後端來新增 endpoint 存取,現在則可以直接在 RSC 完成。這意味著敏感的憑證和密鑰永遠不會暴露在客戶端程式碼中。
import OpenAI from "openai";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY ?? "st-SECRETKEY",
});
// text-embedding-ada-002, text-embedding-3-small, text-embedding-3-large
const MODEL = "text-embedding-ada-002";
export const generateEmbedding = async (value: string) => {
const input = value.replaceAll("\n", " ");
const { data } = await openai.embeddings.create({
model: MODEL,
input,
});
return data[0]?.embedding;
};
const searchDb = async (query: string) => {
const embedding = await generateEmbedding(query);
if (!embedding) {
throw new Error("Embedding generation failed");
}
const sqlEmbedding = pgvector.toSql(embedding);
const results = await sql`SELECT * FROM documents ORDER BY embedding <=> ${sqlEmbedding} LIMIT 3`.values();
return results;
};
export default function OpenaiEmbeddings({query}: {query: string}) {
const data = await searchDb(query)
return (
<table>
{/* data render */}
</table>
)
}由於 RSC 的序列化結構跟客戶端的 streaming 處理都需要有對應的 bundler 來加以處理,我們直接在現有的 SPA 專案是做不到 RSC 的功能的,這也是為什麼 React team 會跟 Next.js 團隊密切合作的關係。現今最快速的使用 RSC 方案就是直接用 Next.js 來完成。
若不想透過 NextJS 來實作 RSC 可以參考先前 React team 所做的 RSC Demo
我們再回頭來看,那 RSC 跟 SSR 有什麼關係,先講結論 - RSC 跟 SSR 是不相干的,但他們可以相互配合來優化使用體驗。
Server-Side Rendering (SSR) 是指在伺服器端渲染整個 React 應用程式的初始 HTML,再將其傳送到客戶端。這種方法的優點包括:
更快的首屏渲染:用戶可以更快地看到完整渲染的頁面,提升了初始加載體驗。
SEO 友好:由於內容在伺服器端渲染,搜索引擎可以更容易地索引頁面內容。
| 特性 | SSR | RSC |
|---|---|---|
| 本質 | 渲染策略 (何時、何地渲染 HTML) | 元件類型 (元件在哪裡執行) |
| 渲染產物 | 完整的 HTML 頁面 | RSC Payload + HTML (混合) |
| 客戶端負載 | 需要下載完整的 JavaScript bundle 進行 hydration | Server Components 不需下載 JS,只有 Client Components 需要 |
| Hydration | 所有元件都需要 hydration | 只有 Client Components 需要 hydration |
| Bundle Size | 所有元件邏輯都需要傳送到客戶端 | Server Components 的程式碼完全不傳送 |
| 資料擷取 | 在getServerSideProps或元件外完成 | 直接在元件內進行,更靈活 |
| 串流支援 | 傳統 SSR 通常需要等待完整 HTML 生成 | 原生支援串流,可逐步傳送 |
| 互動性延遲 | 存在 hydration 延遲,所有元件都需等待 | 只有 Client Components 有 hydration 延遲 |
RSC 和 SSR 並非相互排斥,而是可以互補使用。實際上,Next.js App Router 就是同時使用了兩者:
透過結合 RSC 和 SSR,開發者可以實現更優化的應用程式架構。例如,可以使用 SSR 來渲染整體頁面的骨架,並使用 RSC 來處理某些高負載或不需交互的組件,從而在保持高效能的同時提供豐富的用戶體驗。
現在 NextJS 來到了版本 15,使用 RSC 的 app 架構也 stable 了好一段時間,然而在過去還沒導入 RSC 的架構時我們需要在每個頁面透過 getServerSideProps 來取得資料,但這會導致客戶端的載入需要等拿完資料,畫面才會顯示出來。
在使用 page 架構的 nextjs 中我們可以看到 _app.tsx 這個檔案,裡面的 AppProps.pageProps
import type { AppProps } from "next/app";
const App = ({ Component, pageProps }: AppProps) => {
return (
<Component {...pageProps} />
);
};
export default App;這個 pageProps 就是我們在每個 page 中 getServerSideProps 所取得的資料,每當我們第一次進到該頁或切換到其他頁時會在對應的頁面觸發該方法來取得資料,不但沒 cache 還會因為該方法的回應時間而影響使用者進到下一頁的時間,最影響的就是 First Contentful Paint (FCP)。
import { type GetServerSideProps, type NextPage } from "next";
type Props = {
feeds: any
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const feeds = await getFeeds(context);
if (!feeds) {
return {
props: {
feeds: null
},
};
}
return {
props: {
feeds
},
};
};
const Page: NextPage<Props> = ({feeds}) => {
return (
// ...
);
};
export default Page;然而透過 app 架構的 RSC 我們可以直接在該元件取得 feeds 的資料,若是還沒拿到則用 React.Suspense 來顯示 fallback。
const Feeds = async () => {
const feeds = await getFeeds();
return (
// ...
)
}
const App = () => {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Feeds />
</React.Suspense>
)
}
export default appnext 還可以幫忙 cache getFeeds() 的 feeds 資料,這樣我們在初次載入的時候就可以拿到資料,也有助於 SEO。
過去很多需要透過純後端的優化包含回應時間和 caching 的處理等,現在 RSC 的導入使得 React 這邊有更多空間可以幫忙,當然這也使得 React 的 infra 設置更為複雜,也希望未來這部分能有更好的解決方式。