想真的搞懂「React + TypeScript 做 SSR」(而不是照某個框架教學複製貼上),這篇推薦的主線是:①先用最原生的 Vite + React 親手 wire 一次 SSR——看清 SSR/SSG 在幹嘛、native 工具就能滿足哪些主流需求;②再進最主流的 wrapper——Next.js——看它為什麼要多包這層、大家用它是換到什麼好處。
其餘的選項(Remix、React Router framework mode、TanStack Start…)等你把這條主幹走完、手上有了判斷力,再橫向比較最划算,所以都放在最後的「接下來學什麼」。
為什麼主幹只有「raw SSR」和「Next.js」兩站? 因為先理解問題、再理解 wrapper,才不會把框架的慣例誤當成 SSR 的本質。raw 那一站讓你看見「不靠任何框架,native 工具到底能做到哪」;Next.js 那一站是目前壓倒性最主流的 wrapper(npm 週下載 ~39M、State of JS meta-framework 使用第一),是 CP 值最高的第二步。在第二步,我們也刻意只談框架本身(開源、可自架)、略過只能綁在 Vercel 平台上的服務。其餘框架、以及「Remix 到底去哪了」,放最後延伸。
先備:JS runtime 這層。下面會頻繁出現 Node server、Edge runtime、Web Streams、renderToReadableStream 這些字。若「JS runtime 到底是什麼、Node 跟 Edge runtime 為何不一樣、為什麼 React SSR 有兩個串流 API」對你還模糊,先讀 JavaScript Runtime——SSR 整個討論建在那層之上。
為什麼值得學
- SSR 已經是現代 React 的預設值,不是進階選項。 Next.js 的
create-next-app預設就是伺服器渲染的 App Router。看懂這層,等於看懂現代 React 應用的骨架。 - 想真懂它,最短路是「先手寫、再看 wrapper」。 Vite 官方 SSR 指南自己就說,它的 SSR API「is a low-level API meant for library and framework authors」,做 app 該用更高層的框架——所以親手 wire 一次是為了「看懂」,Next.js 則是「交付」,這個分工官方文件自己就講明了。
- 走完這條主幹,你手上會多一把尺。 之後遇到任何框架(Remix、RR7、TanStack…),你都能問「它在 raw 之上多解了哪個問題?那個問題我到底有沒有?」,而不是被行銷話術或預設值牽著走。
定位與脈絡
先有問題意識:SSR、SSG、CSR、RSC 各在解什麼問題
下手之前,這邊先簡單介紹在解什麼問題、又各自帶來什麼新問題——這串演進史本身,就是你該帶進任何框架的問題意識。
最權威的脈絡整理是 web.dev 的 Rendering on the Web(Addy Osmani & Jason Miller),它把這些做法排成「a spectrum」,用 TTFB、FCP、INP、TBT 幾個指標權衡(定義都在該文)。
一句話抓重點:「One of the core decisions web developers must make is where to implement logic and rendering」——差別只在「在哪裡 render、何時 render」。
- CSR(client-side rendering)——「Rendering an app in a browser, using JavaScript to modify the DOM.」SPA 時代的預設。互動豐富、體驗像 app。新問題:首屏是一張空白頁、要等 JS 下載執行才有內容,SEO 與首屏速度都差。
- SSR(server-side rendering)——「Rendering an app on the server to send HTML, rather than JavaScript, to the client.」解決 CSR 的首屏與 SEO。新問題:(1) 要養一台會跑你 app 的 server(成本與複雜度);(2) 要處理 hydration——HTML內已經有了初次 rendering 的結果,那載入的 JS app 要怎麼綁上去?(3) 收到 request 時才跑 rendering,產 HTML 速度取決於 server 處理的速度
- SSG/靜態渲染(static site generator)—— build time 時就替每個 URL 建好 HTML。解決 SSR 的 server 成本更低,速度更快。新問題:怎麼權衡設計哪些網址要先建 HTML?頁面哪些部分建進 HTML?request 時才知道的動態資料怎麼辦?
靜態 ↔ 動態之間的平衡和組合——早期只能二選一,Next.js:「Prior to PPR, Next.js had to choose whether to render each URL statically or dynamically; there was no middle ground.」後來有了 ISR、串流 SSR、以及 PPR/Cache Components等進階技術:同一頁先送 build time建好的靜態殼,動態區塊隨後串流補上。
主流的 React SSR wrapper 其實不只一個(Next.js、React Router v7、TanStack Start…),但它們全都站在同一組 primitive 之上。要建立判斷力,最短的路是:先用 native 工具看清那組 primitive(第一站),再看最主流的 wrapper 在上面加了什麼(第二站);其餘選項等你有了尺,回頭花幾分鐘比較就懂(最後一節)。
第一站:raw Vite + React SSR——native 工具已經能做到什麼
先把框架全拿掉,用 Vite + react-dom/server + 一支小 server 親手接一次。一次 React SSR 的最小流程長這樣——而 meta-framework 幫你包掉的,就是中間那一整段:
flowchart LR A[瀏覽器請求 URL] --> B[Node / Edge server] B --> C[路由比對 + 先取好資料] C --> D["renderToPipeableStream<br/>(Node 串流)或<br/>renderToReadableStream<br/>(Web/Edge 串流)"] D --> E[瀏覽器先看到 HTML] E --> F[載入 JS bundle] F --> G["hydrateRoot:把事件與狀態綁回既有 DOM"] G --> H[變成可互動的 app] classDef hide fill:#fde,stroke:#c39; class B,C,D,F,G hide
關鍵 primitive(這幾個 API 就是 SSR 的本體):
renderToPipeableStream:Node 環境的串流 SSR 主力(支援 Suspense 串流、onShellReady)。renderToReadableStream:Web Streams/Edge runtime(Deno、Cloudflare Workers)用這個。renderToString:同步、不支援串流、Suspense 支援有限,官方明講「discouraged」,知道它存在即可。hydrateRoot:client 端把伺服器吐出的 HTML「接管」成可互動。官方警告它「expects the rendered content to be identical with the server-rendered content」——hydration mismatch 要當 bug 修,這也是框架最常幫你擋掉的坑。
光是這套 native 組合,就能滿足不少主流需求:伺服器渲染的頁面(SEO、社群分享預覽的 <meta>/OG 標籤都成立)、快的首屏、串流 SSR(搭 Suspense 讓慢的區塊不擋住快的);連 SSG 都不必額外框架——Vite 官方 SSR 指南的 Pre-Rendering / SSG 一節就說:「If the routes and the data needed for certain routes are known ahead of time, we can pre-render these routes into static HTML using the same logic as production SSR.」換句話說,一個內容站、行銷頁、文件站,乃至路由不複雜的中小型 app,raw 工具其實就交付得了。
而開始會痛的地方,正好就是 wrapper 要賣你的:巢狀路由 + 每條路由的 data loading 慣例、自動的 per-route code-splitting、巢狀 layout、快取策略、表單/mutation 的一致寫法,以及把上面這些「一致、不出錯地」接好的那一大坨 boilerplate。Vite 自己也把話講白:SSR API「is a low-level API meant for library and framework authors」、做 app 請改用更高層的框架。這就是進第二站的理由。
第二站:Next.js——為什麼多包這層、換到什麼
目前穩定版是 Next.js 16(2025/10)。把第一站「手痛的點」對照著看,它多包這層 wrapper、替你換到的是:
- 路由與 layout:file-system routing + 巢狀 layout,不用自己 wire router。
- 自動 code-splitting:每條路由自動切包、按需載入。
- 資料與 mutation:RSC(server component 在伺服器抓資料、那段程式碼不進 client bundle)+ Server Actions(
"use server",不必自己手刻 API route 做 mutation)。 - 靜態 ↔ 動態一條龍:把第一站「要嘛全靜態、要嘛全動態」的二選一補成光譜——SSG、ISR、以及 v16 的 PPR/Cache Components(官方:PPR 之前「there was no middle ground」;v16 起快取「entirely opt-in」)。
- 內建優化與 DX:
next/image、next/font、v16 預設且穩定的 Turbopack(dev/build 都更快)、TypeScript-first、跑在 React 19.2 + 穩定的 React Compiler 上。 - 最大的生態與慣例:你會踩的坑幾乎都有人踩過、有現成答案——這本身就是「最主流 wrapper」最實際的價值。
誠實的成本(想避開黑盒的人尤其該知道):RSC 的 server/client 邊界是一套要重學的心智模型;快取曾複雜到 Vercel 自己在 v15 重評預設、v16 改成 opt-in;自架雖可行但有摩擦(v16 才推出 Build Adapters API(alpha) 來改善非 Vercel 部署)。
關於 Vercel:上面講的全是框架本身——開源、可自架。我們刻意略過只能綁在 Vercel 平台上的服務(平台級 analytics、特定 ISR/CDN infra 等):那些是部署便利,不是「學會一個 SSR 框架」的必要部分。學框架本體,你就能把這套能力帶到任何 host。
怎麼入手
「定位與脈絡」鋪好兩站的問題意識後,這節照同一條主幹各動手一次:第一站親手接過 raw SSR、第二站用 Next.js 把同樣的東西重做。順序有意義——倒過來會把框架慣例誤當 SSR 本質。
兩站的 session 切法不同,因為要嚼的東西不同:第一站讀料輕、又跟動手緊綁(每接一個 API 就順手讀那支官方 reference),併成一個「邊動手邊學」的 session 最順;第二站要先消化的概念較重(RSC 的 server/client 邊界、快取模型),值得先用一個 session 純讀懂、對齊,再開一個 session 動手。下面幾段 prompt 各自開新對話、依序貼給 agent。
第一站:raw Vite + React SSR
把任何 meta-framework 拿掉,用 Vite + React + 一支小 server 親手 wire 一次:HTTP → renderToPipeableStream → hydrateRoot。目的不是要你以後都這樣寫 app,而是讓你看清楚 native primitive 在幹嘛、native 工具到底能做到哪、哪裡開始痛——後者就是第二站要被 wrap 的東西。
👉 複製這段 prompt 開一個 session:
我想真正搞懂「React + TypeScript 做 SSR」的最底層——不照框架教學複製貼上,而是把任何 meta-framework 拿掉、親手把最原生的 raw SSR 接出來,邊做邊看清 native primitive 在幹嘛、native 工具到底能做到哪、哪裡開始痛。請帶我邊動手邊學:每一步都說明在做什麼、對到哪個 API、附上引用的官方文件來源,並講清楚現在落在整個 POC 流程的哪一段;每完成一步先確認我懂了、再問我要不要進下一步。不確定我的程度或環境就先問我。1. 先給全貌:用一段話描述我們接下來要做出來的 POC 長什麼樣——一支小 Node server 收 URL → renderToPipeableStream 把 HTML 串給瀏覽器 → client 端 hydrateRoot 接管成可互動,之後再加一頁 build time 預渲染的 SSG。我這時不一定全懂,先有張地圖就好,後面逐步補上。2. 動手做最小可跑的 raw SSR:Vite + React + TS + 一支 Node server,收一個 URL、跑 renderToPipeableStream 把 HTML 串給瀏覽器、client 端 hydrateRoot 接管成可互動。3. 親手感受 hydration mismatch:故意製造一次(譬如 SSR 渲染了當下時間、client 第一次 render 算出來不一樣),讓我看 React 怎麼警告、再把它修掉,並依 React 官方文件(不要憑記憶)說明為什麼 hydrateRoot 要求 server 與 client 初次 render 一致——讓我內化「hydration mismatch 是 bug、不是警告」。4. 試 SSG:依 Vite 官方 Pre-Rendering / SSG 那一節(https://vite.dev/guide/ssr),把其中一頁改成 build time 預渲染的靜態 HTML。5. 檢查:我對上面已有基本理解後,出幾題重點問題問我,讓我用自己的話講一遍;我答完後依 Vite、React 官方文件(不要憑你的記憶)幫我檢查,有需要的話訂正。6. 痛在哪(下一站的引子):這支最小 SSR 之上若繼續長成一個 app,哪些事「明顯不該每個 app 自己接」——譬如巢狀路由、每路由的 data loading 慣例、自動 per-route code-splitting、巢狀 layout、表單/mutation 模式——條列出來,當作進第二站時對照「Next.js 多包了什麼」的清單。最後請根據這個 session 的對話歷史,幫我寫一份交接文件 `raw-ssr-handoff.md`,讓我帶著它開第二站的 session。進行2,3,4時,一邊接一邊帶我讀對應的官方文件、不要憑記憶複述。例如可引用:- Vite SSR guide: https://vite.dev/guide/ssr- React.dev — renderToPipeableStream: https://react.dev/reference/react-dom/server/renderToPipeableStream- React.dev — hydrateRoot: https://react.dev/reference/react-dom/client/hydrateRoot並且記得提醒一些初學者容易誤解或不懂的地方。若這個 session 過長、方向變過幾次,或你判斷 context 壓縮可能影響你的理解,提醒我:換新 session 通常會更穩、更準。提醒時一併把目前狀態、決策、待辦、關鍵檔案路徑整理成一份專案內的交接 Markdown 給下一輪 agent 用;沒有合適專案資料夾或任務很輕,就直接把摘要貼進新對話。不要頻繁提醒,只在真的會影響品質時提。
第二站:Next.js
把第一站做出來的東西用 Next.js(App Router)重做一次,每接一段都明確對到「raw 那邊我自己接的哪一格,現在 Next.js 用什麼 API 幫我做了」。目的是親眼看到第一站列出來的痛點清單怎麼被 wrapper 一一吸收,並順便碰上 Next.js 多帶進來的兩道概念門檻——RSC 的 server/client 邊界與快取模型。
2a. 讀懂 Next.js 在 raw 之上多包了什麼(一個 session)
這站只讀、不動手,目的是動手前先建立唯一非懂不可的心智模型:RSC 的 server/client 邊界。先自己讀下面這幾頁、形成自己的理解,再把 prompt 貼給 agent 幫你對齊、出題訂正:
- 核心(先讀這兩頁):Next.js — Server and Client Components、React —
"use client"——什麼跑在 server、什麼進 client bundle、邊界為什麼要明示。 - 快取只抓一個框架級概念:v16 起快取 entirely opt-in(預設不快取、要快取要明示),掃一眼 Next.js 16 blog 的快取段即可、不必深讀。
- 想再深一層(可選):React — Server Components,但別卡在讀完它。
- routing、巢狀 layout、Server Actions、PPR 這些細節這輪先別讀——下一站動手時自然會碰到。
讀完後,👉 複製這段 prompt 開一個 session:
我已經做過第一站、知道 raw SSR 的 primitive 跟自己接的痛點,手上有第一站產出的交接文件 `raw-ssr-handoff.md`,我會貼給你。這個 session 是動手前的概念對齊,下一個 session 才用 Next.js(App Router)動手重做。我已自己讀過下面這幾頁、想先建立 RSC 的 server/client 邊界心智模型,請在對齊與訂正時以它們為準:- Next.js — Server and Client Components: https://nextjs.org/docs/app/getting-started/server-and-client-components- React.dev — "use client": https://react.dev/reference/rsc/use-client- 快取我只先抓一個框架級概念:Next.js 16 起 entirely opt-in(預設不快取、要快取要明示),來源 https://nextjs.org/blog/next-16請帶我做,不確定就先問我:1. 別先幫我總結、也別憑記憶重述文件——直接用一段話跟我對齊心智模型就好:RSC 的 server/client 邊界(什麼跑在 server、什麼進 client bundle、`"use client"` 把邊界畫在哪一層),以及「v16 快取預設 opt-in」這一個框架級概念。routing / Server Actions / PPR 的細節先不要展開,留到動手那輪;如果你判斷少了哪一塊會讓我下一輪看不懂,再提出來。2. 檢查:出兩三題問我(為什麼 RSC 的 server/client 邊界要明示、`"use client"` 該標在哪一層、為什麼說 v16 快取是 opt-in),我答完後依 Next.js、React 官方文件(不要憑你的記憶)幫我訂正。若這個 session 過長、方向變過幾次,或你判斷 context 壓縮可能影響你的理解,提醒我:換新 session 通常會更穩、更準。提醒時一併把目前狀態、決策、待辦、關鍵檔案路徑整理成一份專案內的交接 Markdown 給下一輪 agent 用;沒有合適專案資料夾或任務很輕,就直接把摘要貼進新對話。不要頻繁提醒,只在真的會影響品質時提。
2b. 用 Next.js 重做 + 碰上概念門檻(一個 session)
這站動手重做,親手碰一次 RSC 邊界與快取這兩道門檻。開始前你應已做完第一站、也跑過 2a 的概念對齊(手上有 raw-ssr-handoff.md)。這站邊做邊學,要讀的官方文件由 agent 在動手時帶你讀,所以下面整段都是給 agent 的——你只要照著貼、跟著做。
👉 複製這段 prompt 開一個 session:
我做過第一站、手上有第一站產出的交接文件 `raw-ssr-handoff.md`,也跟另一個 session 對齊過 Next.js(App Router)的核心心智模型(RSC 的 server/client 邊界、v16 快取 opt-in)。這個 session 我想用 Next.js 把第一站做過的東西重做一次,並親手碰一次 RSC 邊界與快取這兩道門檻。請帶我邊做邊學:每接一段先說它對到「raw 我自己接的哪一格、現在用 Next.js 哪個 API」、附上對應官方文件(不要憑記憶複述),每完成一步先確認我懂了、再問我要不要進下一步。不確定就先問我(如果發現我其實還沒對齊好概念,請先提醒我回去做完概念那一輪):1. 先給全貌:用一段話描述我們接下來會做什麼——用 Next.js App Router 重建第一站那個 app,沿路逐項對到痛點清單,並刻意去碰 RSC 邊界與 v16 快取這兩道門檻。我先有張地圖就好,細節後面逐步補上。2. 動手:用 Next.js App Router 重做第一站做過的東西——同樣的頁面、同樣的資料、同樣能 hydrate 起來互動。每接一段都明確對到「raw 那邊我自己接的哪一格(路由、data loading、code-splitting、靜態/動態邊界),現在 Next.js 用什麼 API 幫我做了」;file-system routing、巢狀 layout、Server Actions 這些我在概念 session 刻意沒先讀的細節,就在這裡邊做邊帶我讀對應官方文件。我會帶著痛點清單進來,請逐項對到 Next.js 的對應做法。3. 踩 RSC 邊界:刻意違規一次——譬如在 server component 裡用 useState、或在 client component 裡 import 只能跑在 server 的東西——讓我看清楚錯誤訊息、用「哪個 module 跑在哪邊」的角度修掉它,內化 "use client" 邊界的意義。4. 看快取:試一個簡單例子體會 Next.js 16 的 cache opt-in 模型——v16 起快取 entirely opt-in,看哪些 fetch / route 預設不快取、要快取要怎麼明示。5. 對照:用一頁說明同樣的需求,何時 raw 就夠、何時值得多吃 Next.js 這層 wrapper 的複雜度,以及 Next.js 為了換到生態與 DX,誠實的成本(RSC 心智模型、非 Vercel 部署的摩擦)落在哪。若這個 session 過長、方向變過幾次,或你判斷 context 壓縮可能影響你的理解,提醒我:換新 session 通常會更穩、更準。提醒時一併把目前狀態、決策、待辦、關鍵檔案路徑整理成一份專案內的交接 Markdown 給下一輪 agent 用;沒有合適專案資料夾或任務很輕,就直接把摘要貼進新對話。不要頻繁提醒,只在真的會影響品質時提。
延伸主題
延伸一:raw + Next.js 之外的框架地圖
走完主幹、手上有了尺,再來看橫向選擇。主流的 React SSR wrapper 不只 Next.js;走完主幹後,其餘三條值得認識的路是:
- React Router v7(framework mode)— 最貼近 web 標準、黑盒最少。 它有三種 additive 的 mode:Declarative → Data(
loader/action/useFetcher)→ Framework(Vite plugin + SSR + type-safehref/Route Module + code-splitting)。官方原話:「pick your mode based on how much control or how much help you want」。由 Shopify 團隊維護,RSC 仍 experimental。(它就是 Remix 的當代延續——若你聽過 Remix、好奇那名字後來去哪了,見文末附註。) - TanStack Start — 型別安全派,但整體仍很早期。 2025/09 v1.0 RC、2026 年中框架本體已到 1.x;賣點是端對端型別安全、isomorphic server functions、串流 SSR,自我定位「no opaque magic」。代價:相對 Next.js 非常年輕,生態、第三方整合與職缺都還小。
- 更前緣的 RSC-first:Waku(Zustand/Jotai 作者 Daishi Kato、建在 Vite + Hono),代表「RSC 不等於只能用 Next.js」。
採用現實(npm 官方 API 單週下載,2026/06 初;react-router 同套件兼作 SPA library 故失真):
| 套件 | npm 單週下載 | 語意 |
|---|---|---|
next | ~39.2M | 最乾淨的 meta-framework 訊號 |
react-router | ~47.5M | 含大量 SPA library 用法,失真 |
@tanstack/react-start | ~14.3M | 框架新,數字含 CI/transitive |
@remix-run/react | ~1.0M | Remix v2 遺產,退場中 |
一句話該怎麼選:最大生態/最多職缺/要學 RSC 前沿 → Next.js(主幹已涵蓋);最透明、最不吃黑盒 → React Router v7;型別控、願承擔較新框架風險 → TanStack Start。
附註:聽過「Remix」的話——它去哪了?
Remix 這個名字,這兩年已經分裂成兩個東西:
| 時間 | 事件 | 對你的意義 |
|---|---|---|
| 2024/05 | 宣布 Remix 併入 React Router:「原訂的 Remix v3 改以 React Router v7 發布」 | 「學 Remix」開始等於「學 React Router v7」 |
| 2024/11 | React Router v7 發布,內含 framework mode(= 原本的 Remix) | 新專案官方建議直接用 RR v7,不要再起 Remix v2 |
| 2025/05 | 宣布全新「Remix 3」:不依賴 React、model-first(為 LLM 最佳化)、建在 Web API 上、從 Preact fork 起家 | 這是另一個東西,跟「React + SSR」無關 |
| 2026/04 | Remix 3 Beta Preview:仍「not production ready yet」 | 想嚐鮮可以,別拿來開新產品 |
所以對「React + SSR」,Remix 的當代延續就是上面的 React Router v7(framework mode);2025 年宣布的「Remix 3」則是另一條與 React 無關的路線。
延伸二:更深的概念
- React Server Components 深入:主幹第二站最大的概念門檻——什麼在 server 跑、什麼進 client bundle、
"use client"邊界怎麼畫。也值得把 2024–2025 期間「直接接 DB / 像 PHP / 隱形 RPC」這些 bad smell 批評找來讀——「概念該懂」跟「該不該用」是兩回事。 - Svelte 5 / SvelteKit:SvelteKit 之於 Svelte,就是這篇的 Next.js 之於 React——把另一個生態的 SSR wrapper 對照著看,更懂「wrapper 到底在解什麼」。