Published on

一探 React 最新的 Server Component 與 Streaming SSR

目錄

背景說明 - Web App 在過去幾年遇到的主要瓶頸

網頁應用程式的前端開發在這幾年愈來愈成熟,各個框架百花齊放、百家爭鳴;然而,在這些突破下,過去幾年仍有一些需要突破的地方,特別是在速度 (being fast) 與動態 (being dynamic) 兩者難以兼得。過去通常需要二者擇一

  • 有速度但沒有動態
    • 過去在使用者進入網站後,要能夠快速地讓使用者能夠取得內容,會透過 CDN 讓伺服器離使用者近一點;只是 CDN 僅能放靜態的資源,換句話說沒有辦法是動態的。
  • 有動態但沒有速度
    • 若要讓網頁是動態的,則沒辦法像是把已經建好的靜態網站放到 CDN 上面,導致動態的內容需要花時間渲染,因此速度會比較慢。

淺談著重動態的 CSR 與 SSR 所面對的速度問題

上面提到,求動態通常會犧牲速度。以電商為例,假如某個頁面需要因應不同的使用者有不同商品推薦內容,我們就需要向伺服器發送請求,透過 API 來獲得這些不是寫死的推薦內容。然而,現在不論是用 client-side rendering (以下簡稱 CSR)server-side rendering (以下簡稱 SSR),都需要透過 API 跟伺服器做請求,來動態地產生內容。然而,當要向伺服器端做的請求一多,速度就會下降,進而導致使用者體驗通常會不好。

  • CSR 的問題

    • 從下圖可以看到,CSR 通常會是先打出一個全白的畫面,接著開始下載 HTML/CSS 以及 JavaScript。這時會遇到的問題時,如果 JavaScript 比較大包,呈現的速度就會慢。而載入完成 JavaScript 後,還要透過 API 向後端請求資料,拿到資料後才可以進行渲染。等這些都完成後,其實已經過一段時間了。因此在使用者體驗當中的重要指標 LCP (最大內容繪製,例如畫面中主要的圖片),會發生在頗後面,導致體驗差。
  • SSR 的問題 - 從下圖可以看到,SSR 會先把向伺服器請求資料的部分處理掉,雖然因為都是在伺服器端,因此做這件事會比在客戶端還快;但做這件事仍是需要時間,如果在使用者剛進到畫面時做,進而花的這了時間,意味著使用者看到 HTML/CSS 組成的畫面的時間也會延後。

    Client-side rendering vs. server-side rendering
    資料來源:Building Blocks of High Performance Hydrogen-powered Storefronts(Shopify)

React 18 透過 streaming SSR 解決這問題

  • React 18 在架構上有了重大的優化,引入了 Streaming HTML 的概念。在談這個概念前,可以先了解幾年前 Web API 新加入的 Streams API。這個 API 強大的地方在於,過往客戶端向伺服器端請求資料時,一次就是請求一包;而若這一包很大,透過網路傳輸就會久。而 Streams API 就是讓請求資料變成像串流那樣,每次請求很小塊,然後邊處理這些資料時,一邊繼續請求。像是下圖這樣:

    Client-side rendering vs. server-side rendering
    資料來源:2016 - the year of web streams

當把 Streams API 的概念導入,我們就可以解決上述 CSR 與 SSR 所遇到的問題。概念上來說,把 Streams API 加入到 SSR,我們不用等整包資料在伺服器端處理完才能傳給用戶端。還記得,上圖要經過 User API、Data API 的資料都取得,並在伺服器端渲染後,SSR 才算完成,這時才會把結果傳給客戶端)。

但下面這個搭配 Streams API 的方法,可以把資料 stream 給客戶端。以下圖為例,User API 的資料拿到後,伺服器端就可以把資料往客戶端傳,客戶端因此能更早呈現畫面給使用者。

Streaming server-side rendering unlocks fast, non-blocking first render
資料來源:Building Blocks of High Performance Hydrogen-powered Storefronts(Shopify)

React Server Component 是什麼?

除了上述的 Streams API 之外,React 近期也提出了實驗性的 Server component,讓上述提到的好處,不是以頁面為單位,而是以元件為單位來達成。還記得上述 CSR 的圖嗎? 之所以在 CSR 的最大內容繪製 (LCP) 會這麼晚才達到,是因為要等所有從 API 回來的資料後,才可以進行繪製。

資料來源:Spotify

我們可能會分成這些元件,並且寫一個 fetch 所有資料的函式,然後把拿到的資料賦到 stuff,這個做法會導致需要等所有資料都拿到後,才能一次呈現。

資料來源:Data Fetching with React Server Components

假如換個寫法,不要把拿資料寫在一起,而是把職責拆到每個元件上,每個元件各自呼叫 API、各自拿資料。

資料來源:Data Fetching with React Server Components

這樣還是沒辦法解決問題,因為還是需要等父層的資料拿到後,才會進一步去呼叫子層。這樣一層一層的,又被稱為 waterfall (瀑布) 問題。並且也有需要多發請求的問題。

資料來源:Data Fetching with React Server Components

不過有了 Server Component,就可以變成,元件跟伺服器拿資料時,伺服器直接在那端渲染。這麼做的好處在於因為都是在伺服器端完成,所以速度會更快。同時搭配上面提到的 streaming,伺服器端渲染完一個子元件,就直接傳回給客戶端,客戶端就直接呈現在畫面上。這樣一來就不會被父層卡住,也就不會有 waterfall 的問題。

資料來源:Data Fetching with React Server Components

除了上述的優點外,Server Component 能進一步讓網頁應用程式更快,原因在於許多套件可以不用像過去 CSR 時都裝在客戶端這邊,而是可以裝在伺服器端。這樣一來,客戶端在最開始,會載到更小 bundle size 的 JavaScript,因此會更快 (以前為了讓客戶端初始的 JavaScript bundle size 變小,還需要額外做 code splitting,在用了 Server Component 後就可以免去這問題)。

下圖看 CSR、SSR、RSC with streaming 差別

下面的影片,第一個是比較 CSR 與 RSC;第二個則是比較 SSR 與 RSC。

所以 SSR 跟 RSC 差別在哪裡?

在 React 核心團隊 Lauren Tan 的這篇貼文中,可以看到兩者的區別

  • SSR 返回的是一个 HTML,雖然這可能看起來會渲染比較快,但若要真正能互動,還是需要等到 JavaScript 都載入到客戶端才能互動。而 React Server Component 返回的是一個 React 可解析的結構,因為這比一般的 JavaScript 量還小,所以速度會比較快。
  • SSR 返回的頁面會讓頁面重新刷新,丢失掉之前頁面上的狀態,比如表單之類的;而 React Server Component 返回的並不會讓頁面重新刷新而丢失狀態。

Shopify 推出的新框架 Hydrogen

https://hydrogen.shopify.dev/

// Product.server.jsx
import { useShopQuery } from '@shopify/hydrogen';
import WishListButton from './WishListButton.client';

export default function Product() {
  const { data } = useShopQuery({ query: QUERY });

  return (
    <section>
      <h1>{data.product.title}</h1>
      <WishListButton product={data.product} />
    </section>
  );
}

// WishListButton.client.jsx
import { useState } from 'react';
export default function WishListButton({ product }) {
  const [added, setAddToWishList] = useState(false);

  return (
    <button onClick={() => setAddToWishList(!added)} type="button">
      {added ? 'Added' : 'Add'} {product.title} to Wish List
    </button>
  );
}

讓速度與動態兼得 - Edge

在最開始談了「速度與動態兩者難以兼得」的議題,接著討論到 React 接下來導入 streaming HTML 以及 Server Component 等方式,讓動態的網頁可以更快速。但目前的前端社群沒有停在這裡,除了上面提到的,還有一個前端人不能錯過的重點詞彙 -- Edge 能進一步協助我們加速動態、動態 的前端。

還記得最開始提到,靜態的網頁速度快,因為 SSG (static site generation) 是把網頁都建好,然後透過佈到各個 CDN,透過 CDN 讓資源離使用者近一點,藉此讓載入速度變快;但同時,因為 CDN 僅能放靜態的資源,其限制是沒有辦法動態。所以如果是行銷內容、部落格這類不太會有變動,且每位用戶都是看到一樣內容的,會很適合 SSG + CDN。

不過在 Edge 的出現後,動態的資源也能做到類似的事情。Edge 是什麼呢? 跟 CDN 很類似,Edge 是存在於客戶端與伺服器端之間的存在。但比起 CDN,Edge 除了存東西之外,還可以處理運算。

這聽起來是不是跟上面提到的概念有可結合之處? 沒錯,Server Component 的概念可以跟 Edge 進一步做搭配。上面提到 Server Component 的好處之一,是讓運算可以在伺服器端進行;所以如果使用者用低階、運算能力不強的裝置,也不用擔心,因為運算是由伺服器來做,所以即使使用者用老舊的裝置,在運算該元件中的邏輯、渲染該元件的速度也不會受影響。而當 Server Component 跟 Edge 結合,就可以讓這個運算在離使用者更近的地方發生,這帶來的好處,就如 CDN 能帶給靜態資源的好處一樣。

這篇分析 Netflix 技術之所以成功的文章中提到 “Anyone who wants to improve performance is going to try to put a server as close to the end user as possible.” 有了 Server Component,前端開發者可以把需要跟後端做請求的元件搬到伺服器,而有了 Edge 則可以進一步讓伺服器更靠近使用者。這對前端開發來說可說是雙重加速!前端開發者也可以不用在速度與動態中做取捨,我們可以兩個都要 :)

今年 Q4 兩個 React 的重點 SSR framework -- vercel 的 Next.js 以及 Shopify 的 Hydrogen,都強調與 Edge 的整合,相信在框架與技術更穩定後,這樣的開發模式會是前端的未來趨勢。

延伸閱讀

參考資料