從 Next.js 13 認識 React Server Components

莫力全 Kyle Mo
37 min readMar 24, 2023

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 開發方式。

在這個 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。

混合式渲染,橘色為 Server Components,藍色為 Client Components

混合渲染是怎麼進行的?

簡單而言可以拆分為以下三步驟:

  1. Render Root
  2. Request For Server Components
  3. React Runtime Rendering

Render Root

瀏覽器拿到頁面 HTML 後,會發出 request 請求主要的 JS bundle, 比如說main.js,這個 JS bundle 包含了 React Runtime 與 Client Root,Client Root 執行後會創建一個 Context,這個 Context 可以用來儲存 Client Side 的 state。

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 開始渲染,並形成一顆混合的元件樹

這顆混合的元件樹會形成類似下方的物件,帶有 React 必要的資訊

module.exports = {
tag: 'Server Root',
props: {...},
children: [
{ tag: "Client Component1", props: {...}: children: [] },
{ tag: "Server Component1", props: {...}: children: [
{ tag: "Server Component2", props: {...}: children: [] },
{ tag: "Server Component3", props: {...}: children: [] },
]}
]
}

不過剛剛有說過 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

剛剛那些 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 不用等到拿到所有資料才能開始做進一步處理。

因此混合渲染的簡易流程會如下圖:

其實以上的流程都只是簡化的版本,中間運作的過程其實需要前後端之間不少複雜的互動,有興趣可以參考這篇文章,而像 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。

import ClientComponent1 from './ClientComponent1';

function ServerComponent() {
return (
<div>
// Client Component 會自動被 Code Splitting
<ClientComponent1 />
</div>
)
}

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 一樣。

(想更了解兩者的區別,可以參考這篇文章。)

Server Components With Next.js 13

Page Directory → App Directory

熟悉 Next.js 的人應該知道 pages 是一個特殊的資料夾路徑,寫在其中的檔案會直接對應到前端應用的 Routing System,舉例來說,/pages/about.tsx 會對應到應用的 /about 頁面,/pages/nested/test.tsx 會對應到 /nested/test 頁面,這樣的方式讓我們不用去另外定義 Router,非常的直觀又方便。

以上的模式也被稱作「Page Directory」,而 Next 13 提出了一個實驗性質的模式 — 「App Directory」。

Next.js 13 App Directory

App Directory 與 Page Directory 在檔案安排上最大的不同就是它讓與 Page 沒有直接相關的檔案也可以放到路徑資料夾底下,這樣 colocation 的功能是 Page Directory 做不到的,因為只要放到 pages 資料夾底下的檔案,都會自動建立出一個路徑出來,所以我們不能把 CSS 檔案或是 components 檔案與頁面檔案放在一起。在 App Directory 中,真正可以決定頁面路由的只有 page.js 這個檔案,也就是說 /app/dashboard/page.js 會建立出 /dashboard 的頁面、 /app/nested/test/page.js 則會建立出 /nested/test 頁面,其他的檔案則不會影響 Routing 結果。

在 App Directory 中,CSS 或測試等檔案都可以 colocate 在一起而不會影響應用路由結果

雖然說現在檔案可以隨意 Colocate,但 Next 有規定一些特定的檔案與它們特別的作用:

App Directory 的保留字檔案

如果都有定義的話,這些檔案也會被 Next 以特定的 Hierarchy 渲染出來,我們稍後也會探索其中的幾個檔案。

App Directory 作為 Next.js 新版的 Routing System,它還提供了一些額外的新功能:

App Directory 帶來的新功能

其中最重要的新功能就屬本篇的主角 Server Components 了,當然,其他的功能稍後也會有各自的段落來介紹(它們也跟 Server Components 的使用息息相關)。

(Next.js 13 以 App Directory 為起始點,其實帶來非常多的新功能與改變,從官方特別為 App Directory 做了一份新的 Document Site 就可以得知這點。筆者在一篇文章內是不可能講完所有功能的,因此感興趣的讀者非常推薦自行去閱讀看看喔!)

Server & Client Components

不同於 React 官方最初提供的範例是以檔名來判斷一個元件是 Server Component 還是 Client Component (ex: test.server.tsx, form.client.tsx),在 Next.js 13 App Directory 中,所有的 Components 預設都是 Server Component,並且 Server Component 可以是一個 async function。而當你認為一個元件需要使用者互動或是呼叫 DOM 等 Web API 時,再透過 “use client” 標記元件為 Client Component。

App Directory 中,元件預設都是 RSC,並且可以是一個 async function
透過在檔案開頭註記 use client 宣告該元件為 Client Component

透過「預設情況下走 Server Component,若有使用者互動的需求則走 Client Component」 這個原則,比起原先需要手動在檔案名稱上註記 Server 或是 Client Component 的方式,我認為可以在一定程度上減少開發者的心理負擔。

Next.js 官方文件也列出了 Server 與 Client Component 的使用時機:

Server Component 與 Client Component 的使用時機

另外,在 Server Component 與 Client Component 的使用上,Next.js 也提出了一些限制,例如說在 Client Component 裡不能直接 import Server Component

而必須以 children props 的方式傳入,因為前面有提到,Server Side 在渲染過程如果遇到 Client Component,它並不會去實際執行或渲染它,而是會把一些資訊註記起來傳給 client side,所以如果是上面的寫法,如果執行 Client Component 的檔案的話,React 是不會知道它在 return function 需要去渲染那個 Server Component 的。透過下圖 children props 這種 pattern React 才知道它需要在回傳資料給 client 前先在 Server Side 渲染這個 Server Component,也就是說 React 透過這種方式才能在 Server Side 完整解析樹狀結構。

另一個限制是雖然 Server Component 可以透過 props 的方式傳資料給 Client Component,但這些資料必須是 serializable 的,所以像是 functions 或 Dates 物件就不能直接 pass 給 Client Component。

官方有提到一個詞要做「Network Boundary」,在過去版本的 Next, Network Boundary 存在於 getStaticProps/getServerSideProps 與 Page Components 之間,而在新版的 App Directory,這個 boundary 則介於 Server Components 與 Client Components 之間。當你想要跨越 Network Boundary 傳遞資料時,這些資料就必須是可序列化的,這就是為什麼 Server Components 傳遞資料給 Client Components 有這個限制的原因。

再來是既然拆分出了 Server Component 與 Client Component的概念,由於 JavaScript Module 是可以被在這兩種元件中共用的,如何去避免只打算在伺服器上運行的程式碼不小心被用戶端執行也是一件很重要的事。舉個例子來說:

這個 data fetching function 初步看下來是可以被 Server 與 Client Components 共用的,但如果你對 Next.js 比較熟悉或是眼睛比較銳利,其實就會發現上面用的 environment variable 並不是 NEXT_PUBLIC 開頭的,按照 Next.js 的定義,這個 env 就只能在 Server Side 才能存取到。所以如果這個 function 被 Client Component 執行,該 env 會得到 undefined,會導致獲得錯誤的執行結果。

為了避免這種 Client Side 與 Server Side 特定 features 誤用的問題,Next.js 官方建議使用 server-only 這個 npm package,它可以在當有誤用的狀況發生時在 build-time 就可以抓到 error (反過來的,其實也有 client-only 這個 package 可以使用)。

透過 server-only package 限制 getData function 只能被 Server Components 使用

Next.js 官方也建議如果可以的話,請盡量把 Client Components 放到 component tree 的葉子節點中,這樣做的好處是可以盡量減少要傳給 client 端的 JavaScript bundle size,對於前端應用程式的效能來說是有幫助的。

共享資料是前端應用一個重要的功能,那麼 Server Component 與 Server Component 之間要如何共享資料呢?講到 sharing data,React 開發者可能都會自動聯想到 Context,不過很可惜,在 Server Component 中是無法使用 React Context 的,但我們卻可以利用一些 Design Patterns 例如 Singleton Pattern 搭配 JavaScript 的 module system 來達成在多個 Server Components 間共享資料。底下的例子就是在示範如何在多個 Server Components 間共享資料庫的連線:

以上的內容大致簡介了在 Next.js 13 中是如何整合 Server Components 這個概念,可以發現比起 React Core Team 當初提供的範例,它已經做出了非常多的修改,例如不用副檔名做區別,還有在 Next.js 中目前並沒有 share component 的概念,而如果不出意外的話,等到 stable 的版本推出,剛剛介紹的使用方式可能又會做出改變,我們必須先有這個認知與共識。

Data Fetching

App Directory 引入 Server Components 之後,最大的改變就是 data fetching 的模式了,而在今天的內容中,針對 data-fetching 我想討論以下幾個重點:

  • The fetch() API
  • 在 Server Components 做 data fetching
  • Component-Level data fetching & Caching
  • Parallel and Sequential data fetching
  • Static & Dynamic data fetching

The fetch() API

在 Next.js Server Components 中,官方建議使用 fetch() 這個 Web API 搭配 async/await 來做 data fetching,不過這個 fetch API 並不是以往我們熟悉的 fetch,而是經過封裝的版本,它主要多了兩個重要的功能:

  • automatic request deduplication
  • 提供更豐富的 options object 讓使用者可以傳入,使每個 request 可以分別設置 caching 與 revalidate 的規則

而這兩點我們會在稍後分別介紹,現在只需要知道在 Next App Directory 中一般常使用 fetch 這個 API 來做 data fetching 就可以了。

在 Server Components 做 data fetching

其實上面的段落在介紹 Server Components 時就有提到,我們可以在 Server Components 做 data fetching,而這樣做有一些好處,例如:

  • 我們對於 backend data resources 有直接的存取權,例如 Database 或是 file system
  • 在 Server Side 可以避免一些敏感資訊洩漏到 client side,例如 access token 或 API key…等等。
  • 讓資料的抓取與渲染在同一個環境中進行,這麼做可以減少 client side 與 server side 間 back-and-forth 的溝通,也可以盡量減少 client side main thread 的工作量。
  • 也許可以在更靠近 data source 的狀況下執行 data fetching,減少延遲以優化效能。
  • 減少 client-server 間的 waterfall 請求 (典型的例子是一層又一層的 useEffect)。
在 Server Components 中可以搭配 async/await 做 data fetching

需要補充說明的是,這並不代表我們就不能在 client components 做 data fetching ,畢竟偶爾還是會有這方面的需求(舉例來說,當使用者點擊時去抓某些資料)。不過 Next.js 官方建議如果要的話使用 swr 或是 react-query 等 third-party library 會比較適合。

在過去,在不使用上述提到的第三方套件的狀況下,如果在 Client Components 裡面我們想要做 data fetching,並且還想要處理 loading 時跟發生 Error 時的狀態,我們可能會這樣寫:

單就 Call API 的情境來說其實有點過於複雜。未來 React 官方也有可能會推出支援處理 promise 並搭配 Suspense 的 use() hook。(詳情請參考 React RFC)。

它的使用方式很像 async/await,並且可以搭配 Suspense 與 React Error Boundary 使用,所以我們就不用自己定義 error 跟 loading 的 state,同一個情境,在單一 component 的程式碼變得簡潔許多。

不過這個新的 hook 仍然在 RFC 的階段,需要靜待它未來的發展了。

Component-Level data fetching & Caching

在過去的 Next.js 版本,我們如果要在 Server Side 做 data fetching,需要透過在 page 的 root file才有提供的一些 built in function 才能做到,例如 getServerSideProps, getStaticProps…等等,可以說 data fetching 的思考模式是 page-level 的,我們需要在 page 的頂層把資料抓取完再依靠 props 的方式傳給底層的元件,但有了 Server Components 之後,我們可以做到更細緻的 Component-Level data fetching。

Next 官方也建議把 data fetching 的 function colocate 在需要這些 data 的 server component 旁邊,例如上一個段落一樣的例子:

假設我們想要在 page 的最上層與頁面底下的某個元件共享同一個 fetch request 得到的資料,我們可以分別在 page component 與 child component 都呼叫這個 fetching function,而不是在上層的 page component 抓取後透過 props 傳到子元件裡,也就是大家一定聽過的 Prop Drilling 這種多層次傳遞資料的模式 (你可能會想到 React Context,但很可惜,在 RSC 裡面是不能用 Context 的)。你可能又會想,這樣不是會打兩次 request 造成效能問題?還記得剛剛提到 Next.js 封裝了 fetch API,實作了「automatic request deduping」,這個概念其實就是 Next.js 實作了一個快取,如果發現是重複的 request(同一個 render cycle 中),就直接回傳快取的結果,而不會再發出一次網路請求。

那如果我們的請求不是透過 fetch API 呢?畢竟在 Server Components 可以存取到的 data source 非常多元,假設是直接存取 DB 呢?要怎麼做到 automatic request deduplication?

React 在某個 RFC 中提出了 cache function,可以將傳入的 function 回傳的結果快取起來,只要帶入的參數不變,就可以從快取回傳值。至於如何 invalidate cache,則要等待 React 社群對這個 API 未來的進展(目前有說會有專門針對這個 API 的 RFC,但在寫這篇文章的當下似乎還沒出來)。

其實剛剛提過 fetch API 之所以可以做到 automatic request deduplication,就是經過這個 cache function 的封裝。

值得一提的是,fetch API 針對 POST 請求並不會做 automatic request deduplication,所以當我們是用 GraphQL 的 POST endpoint 來做 data fetching時,可以透過 cache function 開啟這個行為。

Parallel and Sequential data fetching

提到在 Server Components 中做 data fetching,我們心中要有兩種模式

  • Parallel Data Fetching
  • Sequential Data Fetching
藍色為 request fetching ,紫色為 rendering

透過 Parallel Data Fetching, 同一個頁面的多個請求可以同時被 init 與 load data,這樣避免 waterfall 的方式可以盡量減少總共花費的時間。

有時候你會需要請求是 Sequential 進行的,例如你需要第一個 request 拿回來的 data 作為第二個請求的 input,這種方式通常會需要花費較長的時間。

接著我們來看看在 Next.js Server Components 中分別要怎麼實現這兩個模式。

Parallel Data Fetching

要實現 Parallel Data Fetching,可以在 page component initiate 所有的 promise request,並搭配 Promise.all 等待所有的 promise 都 resolve。

這邊要注意我們是先開始各個請求再呼叫 await,所以各個請求可以同時開始抓取資料,避免 waterfall 的情形。

但以上這種 Pattern 有一個潛在的問題,當其中一個 Promise 需要較長的時間才能 resolve,因為 Promise.all 的關係,我們會卡在那等待,不會進到 return function,因此使用者在所有請求都 resolve 前是看不到內容的,這可能對 UX 來說不是太好的體驗。因此我們可以看另一個版本的寫法:

對 UX 更好的 Parallel Data Fetching

在這個版本中,我們把需要耗費比較多時間的 data fetching promise 丟到下層元件去 await,並用 Suspense 把該元件包起來,這樣在 artistData 先被 resolve 後,使用者可以先看到 {artist.name} 跟 suspense 的 loading UI,以使用者體驗的角度來說是比較好的模式。

(關於 Parallel Data Fetching,官方還有提出一個更進階的 Preload Pattern,有興趣的朋友可以再自行看看)

至於 Sequential Data Fetching 也十分直覺

需要等到上層 page component 先抓取 id 並傳入後才會開始在下層 component 做 data fetching。

這種將 data fetching 寫在下層 component 的寫法,需要等上層的 layout 或是 page component 完成抓取後才能夠開始,造成 waterfall 的情形。

Static & Dynamic data fetching

提到 data fetching,我們可以更進一步思考資料的種類,一般來說,資料可以分為兩種:

  • Static Data: 不會經常變動的資料,例如部落格文章,這種資料就很適合放到快取中。
  • Dynamic Data: 會頻繁更動的資料,例如文章留言。

我們在上面看了不少用 fetch API 抓取資料的例子,Next.js 預設會做 Static 的 data fetching,也就是說 Next 會在 build time 做 data fetching,並且存到快取中並在之後 reuse,這樣做主要有兩個好處:

  1. 減少 server 與資料庫的負擔
  2. 減少頁面的 loading time

不過剛剛也說有些資料是需要頻繁更新的,這時候一律做 static fetch 就不太合理了,開發者應該要對這個行為有更高的控制權。剛剛有提到 Next 封裝了 fetch API,它其實就可以讓我們做到對每一個 request 快取的控制。

透過 fetch API 的第二個物件參數,我們就可以控制每一個請求的快取行為,我們可以發現這些行為跟 Next 過去版本的 getServerSideProps, getStaticProps 可以做到 SSR, SSG, ISR 非常類似,不過過去我們只能控制 page level 的 requests,而在 Next13,我們可以以 per-request 的角度去思考這件事。

關於 Next.js 13 App Directory 的 data fetching,其實還有很多 features 沒有 cover 到,建議有興趣的讀者進一步閱讀官方文件的兩篇文章:

Streaming and Suspense

Next.js 在 App Directory 中,還引入了 Streaming with Suspense 這個功能。

要了解 Streaming 是怎麼運作的,我們得先了解過去 SSR 的運作機制與它所受到的限制。過去如果頁面是採用 SSR,需要經過幾個步驟,使用者才能看見一個完整且能夠進行互動的網頁:

  1. 在 server side 抓取想要的資料
  2. Server render 出 HTML
  3. 頁面的 HTML, CSS, JS 被送到 client side
  4. 使用者這時候可以看到畫面,但還不能進行互動
  5. React 進行 hydration,賦予 UI 互動能力

從上圖就可以看出這些流程是順序執行且 blocking 的,server 必須抓取完所有資料才能 render HTML,而在 client side,React 必須等到當前頁面中所需要的元件的 JS code 都被載入後才能開始進行 hydration。

Next.js 透過這種方式讓使用者可以盡快看到畫面,儘管還是沒辦法互動的,不過以使用者體驗的角度來看卻可以減少用戶等待頁面 loading 的時間。

不過,如果我們需要在 Server Side 抓取大量的資料,那麼頁面的載入就會變得很慢,使用者看到頁面的時間會因此被推延。而 Streaming 帶來了解決這個問題的機會。

Streaming 讓我們把頁面的 HTML 拆分成多個較小的 chunks,並「漸進式」的把這些 chunks 從 server 端傳送到 client 端。

Streaming

這麼做可以讓頁面的部分元件可以比較快速地顯示,而不用等待所有 data fetching 都完成後才能顯示任何的 UI。

而 React 的 component model 其實剛好非常適合 Streaming,因為每個元件我們都可以把它想像成一個 chunk,對網頁應用來說比較重要擁有較高 priority 的元件(例如說產品的資訊)或是不需要抓取資料的元件(例如 layout)就可以優先傳送到 client side 讓 React 更早的處理它們。相較之下,priority 比較低的元件可以在完成 data fetching 或複雜計算後再以 streaming 的方式傳到 client 端。

如果你想要避免耗時的 data fetching block 到頁面的渲染,造成一些指標分數的表現降低,例如 TTFB, FCP, TTI,那麼 Streaming 也許可以給予極大的幫助。

SSR with Streaming

(關於 React 的 Streaming SSR,可以更進一步參考 GitHub 上的 discussion

在 Next.js App Directory 中,我們可以把需要執行一些非同步操作,例如 data fetching 的元件用 <Suspense> 元件包起來,並且指定 fallback 的 UI,例如 loading spinner,並在該元件準備好後替換成完整的內容。

其實 suspense for data fetching 對於 React 官方來說還是一個 experimental 的 feature,Next.js 雖然搶先一步嘗試,但也有很多方面是還沒有很好的 solution 的,例如說官方的文件提供的範例都是在 Server Components 做 data fetching 並搭配 Suspense 顯示 loading UI,至於如果要在 client components 做到類似的事,官方並沒有說明如何做到,我們可能需要剛剛提過的 React 新的 “use” hook 才能協助處理 Promise,而這個 function 還在 React RFC 階段,未來應該還會充滿許多變數。

目前版本遇到的限制

我自己實際用 Next 13 App Directory 開發一些 side project 後,發現現有的版本其實遇到蠻多限制的(另外還蠻容易遇到 Bug 的,不過這就先不談了)。

例如一開始在做技術選型時,我原本想要用自己習慣的 styled-components 這個 CSS-in-JS 的 framework,不過後來發現 Server Components 目前並不支援 runtime 的 CSS-in-JS solution

再來像是在使用很多第三方套件時,如果該套件需要用到 client state 相關的功能,套件維護者卻沒有支援 client components,我們就需要手動做這件事情才能使用它們。

另外現行的版本對於 data 的 mutation 還沒有很好的解決方案,例如 server components 抓取了一個 TODO list 的陣列資料,使用者可以更改每個 todo 的狀態,但目前沒有一個很好的方式去 trigger server components 重新抓取新的資料或是更新快取,只能透過 refresh 的 workaround 方式 (看起來它是極沒有效率的從 Server Component Root 重新抓取一次,而我們希望的應該要是 Partial 的 Sever Components Re-Render),詳情可以看官方的文件說明

這邊只是大概列了幾點 Server Components 在開發上的限制,實際上一定還會隨著開發的應用複雜度提升,會遇到更多的問題。這也許代表離 Server Components 變成穩定且主流的開發方式,我們還需要再等待一段時間。

Future Roadmap

關於 Next.js 13 App Directory 目前已經支援的功能以及正在進行開發的功能,可以參考官房提供的 Roadmap,相信未來支援的功能會越來越穩定與豐富。

結論

未來如果 Server Components 正式推出了,想必會大大改變我們過往熟悉的 React 開發方式。透過這次研究這個新功能,我不禁再次感嘆前端開發真的是一個很有趣的領域,正所謂分久必合,合久必分。前端從一開始的模板語言,到後來推崇的前後端分離,如今看似走了回頭路,卻又實際解決了一些過往處理起來非常棘手的問題。這個功能的推出也宣告著前端開發者不能僅僅著重在純前端的問題,了解基本的後端知識與 JS 的 Server Side Runtime 幾乎成了必備的能力,這部分就讓我們繼續看下去吧!

至於 Next.js 在最新版本提出了整合 Server Components 的 App Directory,雖然並沒有正式發布,也還有很多問題待解決,但可以看出它們對於 Server Components 這項技術的重視與信心,因此我認為在現在就去接觸了解絕對不嫌早!

React.tw 小聚

其實這整篇文章都是我在 2023/03/23 React.tw 小聚所分享的內容,很久沒有實體分享了,非常開心,希望這次的分享有幫助到大家!

(附上這次分享的投影片連結

--

--