Skip to main content

[note] Rendering Pattern (feat Next.js)

page rendering patterns

圖片來源:Next.js: The Ultimate Cheat Sheet To Page Rendering

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 GenerationServer-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:伺服器每次收到請求時在產生對應的靜態頁面。
caution

在開發模式(development mode)下,Next.js 都會採用 Server-side Rendering,即是你設定使用的是 SSG。

img

img

圖片來源:Two forms of pre-rendering @ Next.js

特色

  • 由於「畫面渲染(特別指 HTML)」和「資料拉取」都是在 server 處理(且是在 server 收到 request 後才開始動作),而 DOM 事件是在 client 是才被綁定(稱作 hydration),hydrate 完後使用者才能與頁面進行互動

  • 畫面及內部所需的資料都需要「全部到位」後才會回傳給使用者;該頁面的 JS 和 Hydrate 要「全部處理完」後,使用者才能和整個頁面進行互動

  • 使用者可以在載入和執行(hydrate) JavaScript 前先看到畫面的「內容」,但可能還不能和頁面進行互動

  • Bundle size 小很多

tip

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>;

Streaming Server Rendering with Suspense

在 React 18 前,SSR 需要:

  1. 先 fetch 完所有的資料後,才能顯示頁面:因為 data 都是是在 server 端進行 fetching。雖然我們可以把某些區塊透過 CSR 的方式在來拉取資料(例如,comment section),但這麼做的話,頁面在最開始渲染的時候並不會看到這個區塊;而且,資料必須在 client 才能被 fetch,等於需要花更多的時間。
  2. 先下載完所有的 JavaScript 後,才能開始 hydrate,而且 hydration 是在整個頁面一次處理完。在還沒完成 hydration 前,即使使用者已經可以看到頁面中的內容,但仍無法進行互動。另外,雖然可以透過 code splitting 的方式,後續才載入與處理這個區塊,以讓 hydration 提早被完成,但這麼做會使得一開始 server 回傳的 HTML 無法包含這個區塊的 HTML。
  3. 需要全部 hydrate 完後,使用者才能開始在頁面進行互動
info

由於傳統 SSR 每一個步驟都需要等待前一個步驟結束後才能開始執行,只要任何一個步驟變慢,都會使得使用者說需的時間增加。

在 React 18 之後

簡單來說,在 React 18 以前,並不支援在 server 端使用 Suspense,在做 SSR 時,整個頁面被視為最小單位,每個頁面都需要走完這四個步驟後,使用者才能有完整的體驗;但在 React 18 之後,則是以 component 作為最小單位:

Streaming Server Rendering with Suspense

每一個元件都可以獨立走完這四個流程,只要把這些元件包在 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 的優先順序」。

info

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 完成

參考資料