[note] Rendering Pattern (feat Next.js)
SSG (Static Site Generate)
「畫面渲染」和「資料拉取」都在 build time 是就都出來完,並變成「靜態檔案」,並**不是在 sever 收到 request 後才開始動作**。
- TTFB, FCP, TTI 較好
- 當頁面變的越來越多時,打包也會需要更長的時間
- 資料在 build time 時就已經寫死,沒辦法動態更新
ISR (Incremental Static Regeneration)
可以視為 SSG 的一種特例,差別在於可以設定靜態檔案過期的時間(revalidate
),一旦靜態檔案過期,就會觸發 server 重新 build 一次該靜態頁面。
- 保有 SSG 的好處,但又能夠在一定時間後更新頁面
CSR (Client Side Rendering)
「畫面渲染(包括 HTML 和事件處理)」和「資料拉取」都從 client 處理
- 專案越大,使用者要下載 bundle JS files 也越大
- 當 bundle size 越大時,檔案下載完成前使用者看到的都是空白畫面(或 loading),可能導致 FCP, LCP 和 TTI 都變差
- 畫面上的不同區塊不用一次全部呈現,可以逐塊顯示
SSR (Server Side Rendering)
想解決的問題
- CSR 要下載很大的一包 JavaScript 後使用者才能看到內容(很久的 loading 畫面)
- 改善 FCP 和 LCP
未解決的問題
- TTFB 較差,畫面出現了但使用者仍能無法互動:仍然需要等待 JavaScript 下載、並執行 hydration 後,使用者才能和畫面上的元件互動
重要名詞
- Pre-rendering:指 server 端預先把 client 請求的 HTML 給產生出來,並回傳給 client,然而此時整個畫面因為還沒註冊事件,所以是不具有互動性的。Pre-rendering 又可以分成
Static Generation
和Server-side Rendering
這兩種。 - Hydrate:主要的動作是將 event handlers 綁定在 DOM 元素上,並且準備好對這些事件做反應;其次則是要強化 server rendered 的頁面,例如,自動播放影片、訂閱一些需 live 顯示的資料等等。
實際上,Pre-rendering 分成兩種形式,分別式 **Static Generation(SSG / Static Site Generation)**和 Server-side Rendering(SSR),這兩者間最重要的差別是「靜態頁面(HTML)產生的時間點」。
- Static Generation:靜態頁面是在 build-time 時就產生,伺服器收到 request 時重複使用這些已經生成好的靜態頁面。
- Server-side Rendering:伺服器每次收到請求時在產生對應的靜態頁面。
在開發模式(development mode)下,Next.js 都會採用 Server-side Rendering,即是你設定使用的是 SSG。
圖片來源:Two forms of pre-rendering @ Next.js
特色
-
由於「畫面渲染(特別指 HTML)」和「資料拉取」都是在 server 處理(且是在 server 收到 request 後才開始動作),而 DOM 事件是在 client 是才被綁定(稱作 hydration),hydrate 完後使用者才能與頁面進行互動
-
畫面及內部所需的資料都需要「全部到位」後才會回傳給使用者;該頁面的 JS 和 Hydrate 要「全部處理完」後,使用者才能和整個頁面進行互動
-
使用者可以在載入和執行(hydrate) JavaScript 前先看到畫面的「內容」,但可能還不能和頁面進行互動
-
Bundle size 小很多
SSR 最大的好處是提升使用者體驗,讓使用者可以先看到畫面內容,並同時於背景繼續下載和執行 JavaScript(相較於 CSR 這種需要等待 JS 全部下載並執行完後才看得到畫面)。
實際的流程
- 在 server 端先 fetch client App 所需要的資料
- 接著,在 server 端將 App 的 HTML 產出並回傳給 client
- 接著,在 client 端下載 App 所需的 JavaScript
- 接著,在 client 端將 JavaScript 的邏輯(例如,event handler)和 server generated 的 HTML 進行綁定(又稱作 hydration)
Streaming Server Rendering with Suspense
TL;DR
在 React 18 以前的 SSR 是以「一個頁面」作為最小單位,這個頁面的所有資料都需要在 server 被 fetch 完後,才會產生 HTML 給 client,client 在收到 HTML 後,需要等 JavaScript 下載完後才能開始執行 hydration;hydration 需要一次對整個頁面執行,需要等到整個頁面都 hydrate 完後,使用者才能和頁面進行互動。
在 React 18 後的 SSR,最大的差別在於可以以「一個元件」來當作最小單位,只需要用 <Suspense />
包起來即可,這個最小單位就可各自以非同步的方式去執行 fetch data、render as HTML、load JS、hydrate 的流程。
在 React 18 以前
- 認識 React 18 前的
<Suspense />
,可以想成類似isLoading
// without Suspense
const Comments = () => {
const { isLoading, comments } = fetchComments();
// 雖然可以用 isLoading 達到視覺上類似的 效果,但需要先 render 到 Comments 後才會開始 fetch data
if (isLoading) {
return <Spinner />;
}
return (
<>
{comments.map((comment, i) => (
<p onClick={handleClick} className="comment" key={i}>
{comment}
</p>
))}
</>
);
};
// with Suspense
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>;
在 React 18 前,SSR 需要:
- 先 fetch 完所有的資料後,才能顯示頁面:因為 data 都是在 server 端進行 fetching。雖然我們可以把某些區塊透過 CSR 的方式在來拉取資料(例如,comment section),但這麼做的話,頁面在最開始渲染的時候並不會看到這個區塊;而且,資料必須在 client 才能被 fetch,等於需要花更多的時間。
- 先下載完所有的 JavaScript 後,才能開始 hydrate,而且 hydration 是在整個頁面一次處理完。在還沒完成 hydration 前,即使使用者已經可以看到頁面中的內容,但仍無法進行互動。另外,雖然 可以透過 code splitting 的方式,後續才載入與處理這個區塊,以讓 hydration 提早被完成,但這麼做會使得一開始 server 回傳的 HTML 無法包含這個區塊的 HTML。
- 需要全部 hydrate 完後,使用者才能開始在頁面進行互動。
由於傳統 SSR 每一個步驟都需要等待前一個步驟結束後才能開始執行,只要任何一個步驟變慢,都會使得使用者說需的時間增加。
在 React 18 之後
簡單來說,在 React 18 以前,並不支援在 server 端使用 Suspense,在做 SSR 時,整個頁面被視為最小單位,每個頁面都需要走完這四個步驟後,使用者才能有完整的體驗;但在 React 18 之後,則是以 component 作為最小單位:
每一個元件都可以獨立走完這四個流程,只要把這些元件包在 suspense 中就會自動被如此處理:
<Layout>
<NavBar />
<SideBar />
<Posts />
{/* 讓 React 先去處理其他的元件 */}
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</Layout>
React 會把被 Suspense 包起來的元件視為一個最小單位,意思是告訴 React 不需要等待這個元件,可以先串流該頁面其他的 HTML 給 client。此時 server 會先回傳該 Suspense 的 fallback 給 client(例如,loading 的畫面)。
React 會先處理沒有被 Suspense 包起來的元件,也就是 fetch data、render as HTML、Load JS、Hydrate;對於被 Suspense 包起來的元件來說,則會先以 Spinner 來顯示(render spinner 的 HTML),並繼續在 server 執行 fetching data 的動作,一旦 data fetch 完後,server 會在同一個 stream 回傳 comments 的 HTML(包含 script)。
當有多個元件被 Suspense 包起來時,這幾個元件都會以併發的方式被處理,這就是 React 18 的 Concurrent Feature,這並不是指多個元件會以平行的方式同時被處理,而是 React 會判斷那個元件需要優先處理,且如果過程中偵測到有其他更急迫的任務,它是可以被中斷的,待其他更急迫的元件處理完後,才再回來處理剛剛被中斷的任務。
Selective Hydration 最重要的目標就是要「根據使用者的互動來調整不同元件被 hydrate 的優先順序」。
React 18 Suspense 所帶來的新 feature 和 server component 是兩件不同的事。
也就是說,在 React 18:
- 不需要等到整個頁面都載入後才能 hydrate;先 render 出來的 HTML 可以先被 hydrate
- 由於可以先對載入好的頁面就進行 hydrate,因此使用者就可以先對 hydrate 好的部分進行互動
- React 會根據使用者對元件的互動來調整對不同元件 hydrate 的優先順序
實作
在 React 18 可以:
- Streaming HTML:只需使用 React 提供的
renderToPipeableStream
- Selective Hydration:使用
ReactDOMClient.hydrateRoot
來取代 CSR 時用的ReactDOMClient.createRoot
,並將特地元件用<Suspense />
包起來
透過在 server 端使用 React 18 提供的 renderToPipeableStream
API,完整支援 suspense 和 streaming,其中的主要功能包含:
- 完全支援
<Suspense />
的使用,因此在 SSR 也可以支援 lazy load。 - 使用
lazy
來達到 code splitting,並且不會讓畫面內容閃一下 - 能夠以 streaming 的方式來處理 HTML
範例
透過 Selective Hydration,用 Suspense 把 <Comments />
元件包起來後
- 原本的 SSR:先 fetch data 完成後才 hydrate
- 可以達到先 Hydrate 完成後資料才 fetch data 完成
參考資料
- rendering patterns
- nextjs | Getting Started @ pjchender.dev
- Static Rendering(靜態渲染) @ React 前端讀書會 by Emma
- Understanding Next.js Data Fetching (CSR, SSR, SSG, ISR) @ React 前端讀書會 by Joy
- Progressive Hydration 與 Streaming Server-Side Rendering @ React 前端讀書會 by Ken
- Streaming Server Rendering with Suspense @ youtube
- New Suspense SSR Architecture in React 18 @ react 18
- Upgrading to React 18 on the server @ react 18
- Selective Hydration @ react 18