我們在前幾天已經看了讓畫面可以有互動的 Hydration,在認識 Hydration 的同時,我們也有聊到 Hydration 可能會造成的效能問題,以及可以用什麼方式來解決。今天我們接著再從另一個角度去看,SSR 模式下,可能會產生的其他優能問題,進而來認識 Steaming 和 Suspense 這兩個可以解決效能問題的方式。
在正式開始主題前,我們先來講一個曾發生在我家的小故事。我們家每年過年都會在外面的餐廳吃飯,偶爾遇到餐廳忙不過來、挲草的狀況。我的爸爸又剛好是一位個性很急的人,只要出菜太慢,就會一直起來想要找服務生問狀況。之前剛好就遇到一個狀況,大家都入坐很久,但是餐點遲遲未上,等了好久,才一下子全都上齊。菜上齊之後,也發現到其實有些菜好像有點涼掉了,所以我們懷疑可能是菜都準備好了,才被送上桌。
在這個故事中,對於餐廳而言,可能會是一個比較方便的做法,因為菜準備好,用一個推車一次送上桌就好,但是對於用餐的客人而言,則是一個很不好的方式,因為等餐等太久,某些菜也涼掉了。
好!看完這個小故事,我們繼續回歸正題!
我們先來回顧在 SSR 模式下,讓使用者看到整個完整畫面的過程,再來進一步看看 SSR 模式下可能會遇到什麼樣的效能問題。
在 Next.js SSR 模式下,從發送 HTTP request 到收到完整的 HTML Response 的流程如下:
輸入網址進入網頁 → 對伺服器發送 request → 伺服器產生完整的 HTML 檔案 → 透過 response 把完整的 HTML 檔案和 CSS 等靜態資源發送回來 → 瀏覽器下載 HTML 中用 script 引用的 JavaScript → 將 event Handlers 綁定到 DOM 上(Hydrate) → Hydrate 完成後,使用者就可以和頁面互動
我們昨天主要看的是把 JavaScript 下載下來,到把相關的 event 綁定到 DOM 的 Hydration 過程,也有提到在 Hydration 過程中,會產生的效能問題是「首次可互動時間延遲(TTI, Time to Interactive)」。更白話一點來說的話,就是有可能出現「畫面已經顯示出來了,但是點擊按鈕,或是進行一些操作時,沒有反應」的狀況。
除了這個看到畫面後,卻沒有辦法馬上就可以進行操作的問題外,其實在更早的階段還有一個可能出現的效能問題,這個問題就是當輸入網址進入網頁後,完整的 HTML 沒有辦法這麼快被 response 回來,導致網頁有一段時間會顯示空白畫面的狀況,這個狀況也就會影響到 FCP(First Contentful Paint)。
雖然說完整的 HTML 是在伺服器上被產生出來,可以減少瀏覽器渲染畫面所需要耗的效能,但是如果這個產生出完整 HTML 的時間太長,實際上對於使用者的體驗仍然會有影響。
說到這裡,有沒有覺得這樣的狀況好像和前面提到的故事有點類似?我們可以把使用網頁的使用者當成去餐廳用餐的客人,完整的 HTML 也就是客人點的菜。如果要等客人點的一桌菜全部都煮好才端上桌的話,就會讓像我爸這樣非常急的客人覺得很不開心,覺得服務得很差。
如果全部煮好再送上桌這個服務方式,對於客人而言是一個不好的做法的話,那可以怎麼做呢?改成當一道菜做好了,就馬上送上桌的方式是不是會比較理想呢?
接著讓我們來進入今天的主題,可以讓菜準備好一道就上的方法 - Streaming
。
Streaming 是一種資料傳輸的技術,它讓我們可以將路由分解一個一個小的區塊(chunk),並在等這些 chunk 準備好的時候,漸進式地將它們從 server 端傳輸到 client 端。
因此,如果在 SSR 模式中搭配 Streaming 技術渲染頁面時,即使某個區塊需要較久的時間才能完成渲染,其他已完成的區塊也能先被送到瀏覽器顯示,這樣也就可以避免整個頁面長時間呈現空白的狀態。
這個就像是前面提到過的方式,不等廚師把所有的菜都做好才一次上,而是做好一道菜就上一道菜,讓客人上一道就吃一道。這樣分批次呈現畫面的方式,也就能減少使用者體感上覺得畫面卡住的狀況。
還記得前面有提到 Streaming 的做法就像是在餐廳點餐,煮好一道菜,就上一道菜給客人吃的例子嗎?因為不需要等到所有點的菜都煮好才被送上桌,所以對於客人的體感來說,也就是減少吃到第一道菜所需等候的時間。這個情境在轉換成我們的網頁渲染,也就是減少瀏覽器將使用者看得到的內容渲染到螢幕上的時間,也就是所謂的 FCP(First Contentful Paint)。
這邊必須強調的是 Streaming 只能優化和畫面顯示有關的 FCP 這個指標,並無法優化和 Hydration 有關連的 TTI(Time to Interactive)。即使畫面已經提前展示出來了,仍然需要靠 Hydration 讓網頁能夠有互動的能力。這就像餐廳用餐時的另一個情境,我們可以想像成有一些種類的桌菜,當上菜後,會需要用卡式爐加熱處理,即使菜先準備上桌,服務員沒把卡式爐的小瓦斯裝上,並且開火讓菜被加熱甚至是煮熟,客人還是只能用眼睛看,無法真正吃下肚。
總結來說,Streaming 的主要目的是讓畫面能夠早點顯示出來,也就是優化 FCP,但是首次可互動時間(TTI)仍然取決於 Hydration,並不會因為有 Streaming 而加快。
接下來也透過實際的例子來看看有 Streaming 和 沒有 Streaming 的差異。
這個是最後顯示在瀏覽器上的畫面
首先,我們先來看 response 回來的 HTML 差異。
沒有使用 streaming 時
這裡可以看到回來的 response 是完整的內容,也就代表頁面中的各個區塊即使需要一些時間才能被產生成 HTML,還是會等待每一個部分都被產生完成才返回 response 給瀏覽器。
有使用 streaming 時
當有 Streaming 的時候,則可以發現到返回的 HTML,有一些部分顯示為<div>Loading</div>
,並沒有在返回 HTML 的當下,就等所有畫面都準備好。
接著我們再來看 timing 部分的差異
沒有使用 streaming 時
當沒有使用 streaming 的時候,觀察 timing 可以發現到 waiting for response 的時間比較長,因為必須等到所有畫面的渲染都準備完成,才會返回 HTML。
有使用 streaming 時
有使用 streaming 的時候,觀察 timing 可以發現到 waiting for response 的時間變短了,原因是現在不需要等到所有畫面準備好,才返回 HTML,可以分段返回。也就變得 content Download 的時間反而比 waiting for response 的時間長。
從這兩個實際例子的比較,就可以更清楚感受到有沒有 Streaming 的差異。有 Streaming 時.並不會等到畫面都準備好,才透過 response 把 HTML 返回給瀏覽器,而是會先返回已經準備好的內容。
想要套用 streaming 的效果,需要同時滿足以下兩個條件:
條件一、想要被 streaming 的內容是「Server Components」且有阻塞行為(例如 await fetch()),並且頁面是 Dynamic Rendering。
條件二、使用 Suspense 包裝想要被切小成一個 chunk 的元件,並且加上 fallback 的內容。
這樣搭配下,可以這樣的方式使用
const fetchData = async () => {
const response = await fetch("https://fakestoreapi.com/users/2");
const data = await response.json();
return data;
};
const UserContent = async () => {
const data = await fetchData();
return (
<div className=" border-amber-400 border-4 h-10 w-full">
{data.username}
</div>
);
};
export default UserContent;
<Suspense fallback={<div>Loading...</div>}>
<UserContent />
</Suspense>
在這樣使用之後,當 UserContent 有因為 fetch 資料而沒辦法馬上顯示出畫面時,會先將 fallback 的內容返回給瀏覽器,等到資料回來了,畫面也準備好了,會再把準備好的內容回傳給瀏覽器。
前面提到的兩個條件缺一不可,即使是一個有可能發生阻塞的 Server Component,也還是需要搭配 Suspense 才可以啟用 Streaming 的效果。即使有使用 Suspense 切分界線,如果被區隔的元件是 Client Component,或是一個只有同步內容的元件,一樣無法有 Streaming 的效果。
Suspense 的作用主要是告訴 React 哪些頁面上的內容要被切割成一個小區塊,也就是說必須要 Suspens 來標示出哪些內容可以晚一點再渲染出來。如果沒有額外使用 Suspense 將需要被切割成小區塊的部分標示出來,也就會視為需要整個頁面都準備好,才可以將 HTML 返回給瀏覽器。
一樣用在餐廳吃飯的情境來比喻的話,Suspense 的作用就像是在規範哪幾道菜可以先擺上裝飾 (fallback 的內容) 就好,等到菜真的準備好再送上桌。如果沒有使用 Suspense,那服務生就沒辦法知道哪些菜可以先擺裝飾,而將整桌菜當作一定要全部好了,才一次送上桌。
今天關於 Streaming 的內容就到這裡告一個段落,明天會繼續來看一個和 Streaming 有關聯的渲染技術,也就是 Partial Rendering。
官方文件 - streaming
Streaming Server-Side Rendering