解決了 local storage 呼叫環境以及 server render 內容和 client 內容不一致的問題後,我們來看另個問題:
假如今天網頁是 Pre-Rendering, 其中一個 Server Component 要 fetch 一包很大的資料,那使用者進入頁面後,不就要等很久才能看到畫面嗎?
對,的確會這樣,為什麼呢?我們來看一下 Server-Side Rendering 大致的流程:
以上這些流程是有順序性的,只要前一步還沒完成,下一步就不會執行。 所以當 data fetching 還沒完成,server 就不會進行 render;client 還沒載完全部 components 就不會進行 hydration。
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming )
為了改善這個問題,React 18 推出了新的 SSR 渲染機制 - Streaming,讓網頁可以分段渲染後傳回給 client,讓 client 可以先顯示渲染好的部分,並針對渲染好的部分先進行 hydration。
比方說我們以 components 當作分段單位,就可以達到當某個 component 在 fetch data 時,其他不用 fetch data,或是 data 比較小的 components 可以先渲染完傳回 client,讓 client 可以先顯示渲染好的 UI 並執行 hydration。用戶就不用盯著白頁老半天,不知道發生什麼事。
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming )
單純分段還不夠,假如還在 data fetching 的部分,沒有任何 UI 也蠻奇怪的對吧?因此 React 18 推出了一個特殊 component - <Suspense>
,讓開發者可以自訂 components 載入完成前的 UI。
其實 Suspense 的概念早在 React 16 時就被提出,只是當時必須搭配 React.lazy
使用。
React.lazy
什麼是 React.lazy
呢?當我們的專案 components 很多,打包後的 bundle.js 檔案很大時,有可能導致使用者首次拜訪網站時,瀏覽器需花較久的時間下載 JS 檔, 造成 loading 速度很慢。因此 React 16 時官方推出了 React.lazy
來讓開發者可以較簡單地做到 code splitting,而不用去改 bundler 設定。
簡單來說,當使用 React.lazy 來 import 一個 component 時,瀏覽器會等到第一次渲染這個 component 才會下載它的 JS 檔,從而減少網站首次載入時需下載的 JS 程式碼量,加快網站首次載入的速度。
import { lazy } from "react";
const MarkdownPreview = lazy(() => import("./MarkdownPreview"));
但也因為 JS 檔要重新下載,勢必在切換 route 的時候會產生等待時間,這時考量使用者體驗,我們可能希望等待 JS 下載時頁面能有 loading...等等的提示,而不是直接白頁。於是官方提供了一個特殊 component - <Suspense>
,讓開發者能自定義等待 JS 載入時顯示的 components。
以下方官方範例為例,在 <Biography>
、<Panel>
和 <Albums>
所需要的資料與程式碼都載入完成前,渲染會暫停,並讓畫面顯示 fallback component - <Loading>
,當三個 components 的資料與程式碼都載入完成後,React 會再隱藏 <Loading />
並接著渲染三個 components。
<Suspense fallback={<Loading />}>
<Biography artistId={artist.id} />
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
但 <Suspense>
只有在載入 lazy-loading component 的資料與程式碼,以及 Suspense data fetching ( React 16 時還不支援 ) 時才會觸發,假如使用 useEffect 或 event handler fetch data 並不會觸發 <Suspense>
。
React 16 時還沒辦法做到 Suspense data fetching,所以當時 <Suspense>
必須與 React.lazy 一起使用。直到 React 18 時,官方才開始支援在 Relay, Next.js, Hydorgen, Remix 上使用 Suspense data fetching。
以 Next.js 13 為例,預設會以 route 當作 streaming 的分界。 所以當 route 的 UI 還在渲染時,渲染好的 layout 會先顯示。
除此之外,還記得 Day06 時我們有提到,App Router 有一些特殊檔案嗎 ( ex: page.tsx
, layout.tsx
等等 )?其中一個特殊檔案 loading.tsx
就是用來當作 Suspense fallback component,可以讓開發者自定義 route segment 還在 loading 時的 UI。
所以藉由 loading.stx
,當頁面還在 fetch data 時,可以讓渲染好的 layout 會先顯示,並讓頁面內容顯示 loading.tsx
定義的 UI。
舉例來說,我希望 /products 下的 route segments 都有一個相同的 header 和 footer,於是我在 app/products/layout.tsx
中 import 這兩個 components:
/* app/products/layout.tsx */
import React from 'react';
import Footer from './components/Footer';
import Header from './components/Header';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Header />
{children}
<Footer />
</>
);
}
接著我希望 /products 的內容,包含一個固定的標題字「強檔商品特賣中!」,和一個需要打 API 取得資料的 Server Component - <ProductList>
:
/* app/products/page.tsx */
import ProductList from './components/ProductList';
export default function Page() {
return (
<div className='mt-[100px]'>
<div className='w-full text-center font-bold text-[30px] mb-[50px]'>
強檔商品特賣中!
</div>
<ProductList />
</div>
);
}
UX 考量,我希望等待 <ProductList>
fetch data 時,頁面能顯示 "loading...",這時就可以在 app/products
中建一個 loading.tsx
:
/* page/products/loading.tsx */
export default function Loading() {
return (
<div className='absolute top-1/2 left-1/2 translate-x-[-50%] font-bold text-[30px]'>
Loading...
</div>
);
}
完成後,當我們進入頁面時,header 和 footer 會先渲染完成,而頁面中間尚在等待 data fetching 的地方會顯示 <Loading>
,等頁面內容 render 完後會再轉為 page.tsx
中定義的 UI。
但因為 loading.tsx
是以 route 當作 streaming 的分界,所以「強檔商品特賣中」也必須等到<ProductList>
渲染完成後才會在頁面顯示。假如我們希望將分界改到 <ProductList>
,讓靜態的標題也能先渲染和顯示,可以直接使用 <Suspense>
:
/* app/products/page.tsx */
import { Suspense } from 'react';
import Loading from './components/Loading';
import ProductList from './components/ProductList';
export default function Page() {
return (
<div className='mt-[100px]'>
<div className='w-full text-center font-bold text-[30px] mb-[50px]'>
強檔商品特賣中!
</div>
<Suspense fallback={<Loading />}>
<ProductList />
</Suspense>
</div>
);
}
所以回到最開頭的問題,假如頁面某個 Server Component 要花較久渲染時間,就可以使用 loading.tsx
和 <Suspense>
來讓其他部分先渲染和顯示。
但學會 streaming 後,我心中產生了另個疑問:既然都把頁面分段了,除了渲染以外,也可以做 error handling 嗎?
這部分就留到明天和大家分享囉!
謝謝大家的耐心閱讀,我們明天見!