发布于:2023-11-19最后更新:2023-11-19W.C. 00 min

type
status
date
slug
summary
tags
category
icon
password
React Server Components 其實不是一個新概念了,React 官方第一次提出這個技術得回溯到 2020 年的 12 月,時至今日已經超過兩年了。不過這個技術卻一直待在研究與開發的階段,沒有正式的發布,可以說幾乎就要被 React 開發者們給遺忘了…,直到去年底 Next.js 13 的發布,部分新功能整合了 React Server Components,才讓 Server Components 又再一次受到關注。
React Core Team 本來就有提過會跟 Next.js 或 Remix 這類成熟的 meta frameworks 合作,在這些框架中嘗試一些未來想要推出的功能與架構,所以我們對於這次 Next.js 13 搶先推出 Server Components 其實不用感到太訝異。
雖然 Next 13 這次推出的關於 Server Components 的功能都還是實驗性質,未來各種 API 都有可能會做出改變,很多部分甚至還有嚴重的 bugs,但我認為這是一個認識 React Server Components 的好時機,我們也可以透過 Next.js 目前整合的現況預測與想像未來用 React 開發應用程式會與現今我們已經熟悉的模式有什麼樣的區別。
這篇文章會簡單複習一下 React Server Components 的概念,並探索 Next.js 13 整合的狀況以及在開發上帶來的改變,最後可能也會分享一下自己試用後的心得感想。

什麼是 React Server Components?

(如果想要更詳細地理解 Server Components,建議把官方的介紹影片看過一次)
React Server Components 是一種新型態的 component,過往我們熟悉的元件則相對被稱作 Client Components,它還是一個實驗性質的功能,不過不出意外的話這個 feature 會是 React 未來發展的方向,如果穩定發布後,也許會重新定義前端與後端的分工,也會改變我們熟悉的 React 開發方式。
相信這幾週大家都應該已經看過這張圖:
Sam 討論了 Next.js 如何實現 React 的未來願景,並且特別關注如何將 Server Components 與 Server Actions 整合至 Next.js 當中。 他解釋稱,Server Components 與 Server Actions 提供 “一流的數據獲取和處理方式”,對 React 的固有強項可組合性做出有力補充。
這個 Tweet 在網路中引發了不少爭議。 但如果大家耐心觀看演講,就能清楚理解 Sam 想表達的觀點:Server Actions 是個好設計!
 
在這個 model 底下,我們可以將元件區分為以下三種:
  • Server Components (.server.js 結尾):在 Server Side 渲染的元件,具有訪問 DB、file system 的能力,但沒辦法做事件綁定,也就是缺少了「互動性」。
  • Client Components (.client.js 結尾):在 Client Side 渲染的元件,擁有互動性。
  • Share Components:可以在 Server Side 也可以在 Client Side 渲染,具體要看是什麼元件引入它,如果被 Server Components 引入就由 server 渲染,反之則由 client 渲染。
我們可以把這個 model 下的 React Component Tree 看成它是由 Server Side 與 Client Side 混合渲染的一個樹狀結構,React 在 Server Side 將 Server Components 渲染好後傳給 client 端,如果 server side 在渲染的過程遇到 client components,它就會用一個 placeholder 來標注它(請注意它不會實際執行或渲染它),未來讓 client side 知道這是需要它來渲染的元件。一般來說 client 端在接收到 JS bundle 後會進行 hydration,不過 Server Components 只會在 Server Side 做渲染,不會在 client 端進行 hydration。
notion image
混合式渲染,橘色為 Server Components,藍色為 Client Components

混合渲染是怎麼進行的?

簡單而言可以拆分為以下三步驟:
  1. Render Root
  1. Request For Server Components
  1. React Runtime Rendering
Render Root
瀏覽器拿到頁面 HTML 後,會發出 request 請求主要的 JS bundle, 比如說main.js,這個 JS bundle 包含了 React Runtime 與 Client Root,Client Root 執行後會創建一個 Context,這個 Context 可以用來儲存 Client Side 的 state。
notion image
Request For Server Components
Client Root 的程式碼被執行後,同一時間,瀏覽器也會向 Server Side 的某個 API endpoint 發出一個請求,也就是上圖的 useServerResponse(以官方範例來說,這個 endpoint 是 /react),當然在打這個請求的時候也需要帶一些 data 過去,可以預料到這些 data 會包含一些 React Tree 的訊息,Server Side 才知道要渲染哪些 components。伺服器端接受到請求後就會開始進行 Server Components 的渲染。
伺服器會從 Server Component Root 開始渲染,並形成一顆混合的元件樹
notion image
這顆混合的元件樹會形成類似下方的物件,帶有 React 必要的資訊
不過剛剛有說過 Server Side 在渲染的時候如果遇到 Client Components,只會用 placeholder 做一個註記, 這些 Client Components 需要送到 Client Side 做渲染,也就是說 React 必須將這些資訊也送到 Client Side,所以 React 最後回傳的其實是一個可序列化且帶有特殊格式 JSON chunk response,以便於之後可以漸進式的在 Client 端渲染。
其中的英文符號代表不同的資料模型,例如 M 代表 module (也就是 Client Components 所需的 JS Chunk 資訊),S 代表 Symbol,E 則代表 Error,J 代表 Server Components 渲染出的類似 react element 格式的字串,React 會根據這些 chunk response 來渲染對應的 Native Elements 與 Client Components。有興趣的讀者可以再進一步參考 React 的 source code
React Runtime Rendering
notion image
剛剛那些 JSON response 送到 Client Side 後,React Runtime 就接手開始工作,他會依據 chunk response 的內容渲染出真正的 HTML 元件。例如當它看到代表 Module 的 「M」,就會發送請求獲取 Client Components 所需的 JS Bundle,當瀏覽器載入 Client Components 的 Bundle 後,React 就可以進行渲染與 Hydration。如果看到代表 Server Components 渲染出的內容的 「J」,React 就會將實際元素渲染出來。值得注意的是,React 在傳輸剛剛提到的 JSON chunk response 是採用 streaming 的方式,也就是說 React Runtime 不用等到拿到所有資料才能開始做進一步處理。
因此混合渲染的簡易流程會如下圖:
notion image
其實以上的流程都只是簡化的版本,中間運作的過程其實需要前後端之間不少複雜的互動,有興趣可以參考這篇文章,而像 Next.js 這種 meta framework 其實會與 module bundler 整合,把這些複雜的流程抽象化與簡化,讓我們未來在使用 Server Components 開發時不用面對那麼底層複雜的問題,這也是 React 官方積極與這些 framework 合作探索這個新功能的原因之一。

React Server Components 帶來什麼優勢?

React Server Components 主要解決了以下幾個問個:
  • 減少 bundle size
  • 運用 Server Side 的能力
  • 自動化 Code Splitting
減少 bundle size
通常在開發前端應用的時候,我們會安裝許多 dependencies packages,有些 packages 甚至還沒辦法做 tree-shaking,隨著引入的套件變多,應用的 bundle size 也會跟著增大,造成頁面載入的效能下降。
Server Components 因為只在 Server 上做渲染,所以元件的程式碼 bundle 不用被下載到 Client Side, 如果套件只被 Server Components 使用(React 官方的範例是一個處理 markdown 語法的套件),就不用擔心它會增加應用整體的 bundle size。
一個更加極端的例子是應用中大部分的元件如果都沒有跟使用者互動的需求,那這些元件都可以使用 Server Components,這些 UI 都可以走「瀏覽器接受 JSON chunk response,React Runtime 接手渲染」的這個模式,這樣的話理論上除了 React Runtime,是不需要其他 JS bundle 的(而 JSON response 體積很小跟 JS bundle 不是一個量級,可以忽略不計)。因為 React Runtime 的 bundle size 不會隨著專案的擴展而變大,所以這是官方號稱 RSC 為「Zero-Bundle-Size Components」的原因。
運用 Server Side 的能力
在 React Server Components 中,我們可以直接 access DB,甚至也可以 access file system,簡單來說你透過 Node.js 能做到什麼,Server Components 就很有可能也做得到(當然現在大多數還是聚焦在 data fetching 這個應用場景上,但如果腦洞大開,其實要在 RSC 做一些複雜的運算也是有可能,但這就等待未來發展了)。透過這樣自由整合後端的能力,我們可以解決 Client-Server 往返過多,甚至造成 waterfall 請求的狀況,典型的情境就是透過 nested 的 useEffect 來 call API 獲取資料,我們需要等在上層的 component 抓取資料並 render 出下層 component 後,才知道下層的元件需要什麼樣的資料,這種模式對效能來說可能會產生很大的影響。
自動化 Code Splitting
在過去我們要做到 Code Splitting 必須自己用 React.lazy 搭配 Suspense 或是使用成熟的第三方套件例如 loadable-component,這樣的缺點就是需要由開發者自行手動分割、自行確認要分割的邊界。
有了 React Server Components,這個麻煩似乎得到緩解,React 將所有的 Client Components 視為潛在的 Code Splitting 分割點,我們只需要按照拆分元件的思維去組織專案,React 會自動幫我們做到 Code Splitting。

React Server Components 的缺點

最顯而易見的缺點就是開發者的學習路徑變得更陡峭了,Server Components 也需要跟 module bundler、伺服器端做整合才能夠使用,在設定上肯定會增加不少複雜度。
另外實際開發時拆解 component 的時候還要進一步去思考「我這個元件要用 Server Component 還是 Client Component ?」這不外乎會增加開發者的心理負擔。

React Server Components vs Server Side Rendering

這兩個名詞很容易讓人混淆,SSR 的機制是在 server 中 render 一個 HTML 傳送到 client side 並在 client side 透過 react runtime 做 hydration,完成之後,頁面才是一個可以互動的完整應用。除了 initial page load 以外,後續頁面的 navigation,例如在 Next.js 中透過 Next/Link 進行頁面跳轉,它其實是走 client side 的 navigation,Next 會 call 一個 API endpoint 去執行 getServerSideProps function 去抓取需要的資料,但並不會重新產生一個 HTML。所以說即便是 SSR 的應用,在 navigation 時我們的 web app 其實就跟一般不是走 SSR 的 SPA 頁面行為一致了。因此我們可以發現,SSR 的重點在於「頁面的初始渲染」。
而 React Server Components 永遠都是在 server 上渲染的,當這些元件需要 re-render 時,它們會在 server side 重新做 data-fetching,然後重新 merge 回 client side 中現有的 React Component Tree 。值得注意的是就算頁面中部分 Server Components 重新再跟伺服器要資料,與此同時 client side 的 state 是可以被保留的。
簡單來說兩者是完全不同的概念,也不互相衝突,使用 Server Components 不一定要走 SSR,使用 SSR 也不一定要用 Server Components,當然兩者也可以結合使用,就像稍後會介紹的 Next.js 13 一樣。
(想更了解兩者的區別,可以參考這篇文章。)

Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉新版博客即將到來🎉
-- 感谢您的注意 ---
👏為您帶來愉快的體驗👏