以下以 RSC 代稱 React Server Component
Next13 的渲染策略與 RSC 有著密切的關係,今天會探討 RSC, fetch 如何影響 Next13 的渲染策略,以及我們可以如何優化。
看完這篇文章預期你會有以下收穫:
RSC 有兩大特點:
RSC 就是運行在server 的 component,擁有運用 server 的能力,可以直接 fetch data, 與資料庫溝通等等。
另外一個非常重要的特性就是RSC 的結果可以被 cache,而且這是 server-level 的 cache,所以另一個 user 在也能在後續使用到這份 cache 的結果,不會侷限於某一個 user 才能使用。
RSC 還有很多優點,但這篇主要探討 Next13 的渲染策略,優點就先列在下方,可以先跳過這段,有興趣再看就好。
Data Fetching
可以在 server fetch data、與資料庫溝通或其他 server 做得到的事。
安全性
隱藏敏感資訊(ex: token)及商業邏輯。
後面會講到 RSC 只會傳送結果到 client,中間的過程只會運行在 server。
快取cache
server-level 的快取。
慢~就給第一個人慢就好了,後續的人可以爽爽訪問。
Bundle Sizes
過去 CSR 最不能解決的問題就是載入過多的 Javascript。
特別是某些特別肥大的第三方套件無法做 tree shaking 或 splitting 時,這個狀況幾乎解決不了。
但 RSC 在 server 上運行完後只會返回 UI 的渲染結果,大大減少了過往 CSR 的 javascript bundle 大小。
畫面首次渲染速度
直接傳送 html 到 client,讓使用者先看到畫面。
相比於 CSR 需要傳送超肥 javascript bundle,還要在 client 組 html。使用 pre-rendering 的方式,畫面首次渲染的速度會比 CSR 快上許多。
SEO
網頁爬蟲在爬到有做 pre-rendering 的網頁時可以看到組好的 html,這有助於 SEO。
Streaming
RSC 與 Suspense 搭配,讓頁面可以分區載入,大幅提升使用者體驗。
首先,我們要理解 RSC 的渲染單位 - chunk。
預設同一個路由底下所有 RSC 是一個 chunk。同一個 chunk 裡的結果會一起被送到 client 端,如果當中有 async RSC 需要等待,那整個 chunk 會等所有 RSC 都渲染完成,才會將結果傳送到 client 端。
底下以一個 about 的頁面為例,AboutPage
是一個 RSC,裡面包含了兩個 RSC 組件。在預設的情況下這個 router 的頁面會等到所有 RSC 渲染完成,才將結果傳到 client。
// app/about/page.tsx
// 整個 Router 是一個 chunk,會等到 AboutPage, MyFamily, MyDog 這三個 component 都渲染完成才會一起傳到 client 端顯示
import MyFamily from "./MyFamily";
import MyDog from "./MyDog;
export default async function AboutPage() {
return (
<>
<h1>關於小熊</h1>
<MyFamily />
<MyDog />
</>
)
}
上面這種渲染方式與 Next12 的 page router 有著驚人的相似,需要等待所有非同步的結果都取得後,才能將這次渲染的畫面一次傳到 client。
我們可以利用 Suspense 包住 RSC,這樣被 <Suspense></Suspense>
組件包起來的部分就會成為一個新的 chunk。
以剛才同樣的範例,為 <MyDog />
組件加上 <Suspense></Suspense>
,把 <MyDog />
從原本的 router chunk 切分出來,成為一個新的 chunk。
Suspense 是 React 提供的 Component,只要包住 async component,傳入
fallback
UI,就可以在 async component 渲染完成前顯示 fallback UI。
// app/about/page.tsx
import { Suspense } from "react"
import MyFamily from "./MyFamily";
import MyDog from "./MyDog;
export default async function AboutPage() {
return (
<>
<h1>關於小熊</h1>
<MyFamily />
<Suspense fallback={<div>loading...</div>}>
<MyDog />
</Suspense>
</>
)
}
簡單來說同一個 chunk 會被當成一個渲染單位,當同一個 chunk 裡有一個 RSC 的 cache 資料過期了,那整個 chunk 都要重新渲染。
更詳細的渲染過程可以看這篇 Next 的官方講解文章
RSC 有三種渲染策略:
RSC 使用的渲染策略連帶也會影響到 Router 的渲染策略。
這裡要先講到 fetch
這個 Next 內建的 function,它除了繼承了 Web API - fetch 的所有功能外,它還與 cache
集成,讓我們可以設定 cache 和 revalidation data。
透過 data 被 cache 的狀態,也會影響到 RSC 的渲染方式。
fetch
預設是 cache: 'force-cache'
,如果參數一樣就會直接返回 cache 裡的結果。
// 預設就是 force-cache
fetch('https://jsonplaceholder.typicode.com/users', { cache: 'force-cache' })
// 設定 revalidation data
// 底下的 data 如果參數一樣,超過 60 秒會過期
fetch('https://...', { next: { revalidate: 60 } })
比較不常變動的資料,可以在 build time 就先把 RSC 的結果 cache 起來。
或是設定 data revalidation,觸發條件才會重新 fetch 新資料,否則可以直接使用 cache 的結果。
// 啥都不加就是 'force-cache',RSC Result 會被 cache
fetch('https://jsonplaceholder.typicode.com/users')
// 加上驗證條件,超過時間才會重新建立 RSC Result
fetch('https://...', { next: { revalidate: 60 } })
Static Rendering 很適合用在部落格文章或不常更動的資料。
需要時常更新的資料,或是需要透過 URL search params, cookie 才能取得的資料。
如果 RSC 內使用 'next/headers'
相關的方法,或是 fetch 設定 cache: 'no-store'
,這都自動將 RSC 由 Static Rendering 轉換為 Dynamic Rendering。
import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies(); // 偵測到使用 header 相關的方法,自動轉換成 dynamic rendering
const theme = cookieStore.get('theme')
return '...'
}
fetch
設定 cache: 'no-store'
import { cookies } from 'next/headers'
export default async function Page() {
const result = await fetch('https://jsonplaceholder.typicode.com/users', { cache: 'no-store' }); // 不使用 cache,每次都會取得最新料
return '...'
}
Streaming 其實就是上面提到的,利用 Suspense
將頁面切分成不同的 chunk,這樣可以讓先渲染好的 chunk 先傳到客戶端渲染,增加使用者的體驗。
另外實作 Streaming 時可以使用兩種 pattern 實作:
這裡就不展開,好奇的可以看從 Next.js 13 認識 React Server Components,裡面有這兩種 pattern 的實作方式
如果 RSC 都是使用 static rendering,完全沒有使用到 dynamic rendering,那這個頁面就是 SSG(Static Site Generation) 或 ISR(Incremental Site Rendering),端看 fetch 有沒有設定 revalidation data,有的話就是 ISR,沒有的話就是 SSG。
接下來 RSC dynamic rendering 就需要稍微討論一下。
定義上只要有任何一個 RSC 是 dynamic rendering,那這整個 router 都會算成是 dynamic rendering,但昨天也有提到,大部分的網頁都會同時包含 dynamic 與 static 的部分。
In most websites, routes are not fully static or fully dynamic - it's a spectrum. For example, you can have e-commerce page that uses cached product data that's revalidated at an interval, but also has uncached, personalized customer data.
而 RSC 也能輕鬆實踐這個理念,我們不必糾結 router 要整頁 Static 還是整頁 Dynamic。這兩個可以混合著使用,在 dynamic rendering router 中使用 static RSC,再搭配 Streaming 的作法,讓靜態的內容先給使用者看到,再逐步顯示 dynamic 的部分,整個使用者體驗大加分!