iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Modern Web

從 React 學 Next.js:不只要會用,還要真的懂系列 第 14

【Day 14】Next.js 的 Cache 機制 (下) - Data Cache & Router Cache

  • 分享至 

  • xImage
  •  

今天我們再來繼續看 Next.js 另外兩個層級的 Cache,也就是「Data Cache」和「Router Cache」。在正式進入今天的內容前,我們先來快速回顧一下昨天說的內容。

前情提要

Next.js 為了優化效能,預設會使用各種不同層面的 Cache 方式。這些不同層面的 Cache 方式,分別是 「Full Route Cache」、「Request Memoization」、「Data Cache」、「Router Cache」

當使用者進入頁面時,會查看要進入的 route,有沒有跳出 Full Route Cache 的動態行為或設定,像是使用 searchParams,或是有 dynamic = 'force-dynamic 的設定等...。 如果沒有跳出 Full Route Cache 的設定,Next.js 會看有沒有對應的 HTML,如果有的話,會返回現有的 HTML,不重新進行畫面的渲染;如果跳出了 Full Route Cache了,則會依照依渲染模式來處理,如果是 SSG 會直接提供 build 階段產出的 HTML,如果是 ISR 會於首次請求時,在 runtime 渲染並將這次渲染的結果儲存請來,之後的請求就能使用儲存請來的頁面,若 Cache 過期時,會於背景重新渲染新的版本儲存。如果是 SSR 則每次請求都會重新渲染,不會進入 Full Route Cache。

如果跳出 Full Route Cache 或有需要進行畫面重新渲染的動作時,會由 React 接棒進行 Cache 部分的處理,這時候如果在同一次的渲染中,有完全相同的 Get Fetch 動作的時候,React 會只會執行第一個 Fetch,並將第一個 Fetch 的結果儲存起來給當次渲染的其他相同 Fetch 使用。 也就是說在同一次渲染中,即使相同的 Get Fetch 被在程式碼中被寫了三次,甚至是四次,實際上只會呼叫一次,因為只有第一次 Fetch 會被執行,其他次的 Fetch 都是使用第一次 Fetch 返回的結果。如果實務上需要跳過這樣的避免重複打多次 Get Fetch 的 Cache 機制,也可以使用 AbortController 的 signal 來跳過這個 Caceh。

真正開始 fetch 資料階段的 Cache - Data Cache

如果進入了畫面渲染,也沒有進入了真正要打 API 的這個步驟了,接下來就是所謂的 Data Cache

Date Cache 從字面上看來,跟我們昨天最後看的 Request Memoization 有點類似,的確都是和 Fetch 資料有關,但其實兩者之前是有差異的。

昨天我們提到的 Request Memoization 是在同一次渲染中,將相同的 Get Fetch 做去重的動作,也就是只會將同一次渲染中相同的 Get Fetch 的其中一次 Fetch 返回的結果 Cache 住,但這個 Cache 住的內容,並不會被運用到下一次重新渲染中,每一次渲染都是一個獨立的 Cache 單位。

但是今天要看的 Data Cache 的則是跨請求的「資料快取」,不限於單次渲染的期間。也就是說當你真正執行 Fetch 動作的時候,會去看這個 Fetch 有沒有 啟用 Cache (例如:force-cache、或 next:{ revalidate: 指定的時間 }),如果沒有啟用 Cache,不會進入 Data Cache 的階段,如果有啟用的話,會看之前是不是已經有 Cache 住的資料,如果有 Cache 的資料,就直接使用之前返回的結果,沒有 Cache 資料,則會進行 Fetch 的動作,並且把返回的資料儲存起來。這個儲存的資料,並不會只使用於單次渲染中,而是會使用於下一次渲染時。

這次我們一樣用例子看看有無 Data Cache 的差異是什麼。
∙ 透過 no-store 明確表示不需要 Data Cache

export default async function Page() {
  const res = await fetch("http://localhost:3000/api/time?key=abc", {
    cache: "no-store",
  });
  const data = await res.json();
  return (
    <div className="w-full h-[300px] flex justify-center items-center">
      <p className="text-2xl">現在時間:{data.ts}</p>
    </div>
  );
}

https://i.imgur.com/zl4RlhP.gif
因為明確表明不要使用 Data Cache,所以每次重整都會打一次 API,取得當前的時間,畫面上的現在時間也就會一直有變動。

∙ 透過 force-cache 明確表示我需要 Data Cache

export const dynamic = "force-dynamic";

export default async function Page() {
  const res = await fetch("http://localhost:3000/api/time?key=abc", {
    cache: "force-cache",
  });
  const data = await res.json();
  return (
    <div className="w-full h-[300px] flex justify-center items-center">
      <p className="text-2xl">現在時間:{data.ts}</p>
    </div>
  );
}

https://i.imgur.com/EFHOXHB.gif
改成使用 cache: "force-cache"明確表示需要 Data Cache 後,可以發現不管怎麼重整頁面,顯示的時間都固定,不會有變動了。

Client 端的 RSC Cache - Router Cache

前面我們都是在說 Server 端的部分,現在終於到了 Client 端的部分了。

當輸入網址,或是重新整理頁面時,我們都會取得一份完整的 HTML 和內嵌在 HTML 的 RSC Payload。當我們在 Client 端上取得 RSC Payload 時,會將 RSC Payload 反序列化為 React Element Tree,並且把這個當前頁面的 RSC Payload 進行 Router Cache 的初始化。

若是畫面中可視範圍內有出現 <Link>,且這個 <Link> 沒有手動設定阻止 prefetch,就再會對這個 <Link> 對應到的頁面進行 prefetch 取得相應頁面的 RSC Payload,並將這個 RSC Payload 也存在 Router Cache 中,當我們在前端畫面透過 Link 進入頁面時,就會使用 Router Cache 的內容。如果在切換頁面時,沒有命中 Router Cache 時,會重新再對 Server 發送 RSC request,這個時候就會重新再經歷是否跳過 Full Route Cache 的階段,最後再把返回的 RSC Payload 儲存在 Route Cache。

與 Full Route Cache 不同的是 Router Cache 的單位是以「使用者在客戶端切換的分頁」為基準;而 Full Route Cache 則是發生在伺服器端,可以跨不同分頁與不同使用者共享 Cache。

簡單來說,Router Cache 主要是在 client 端的 Cache 機制,當關掉當前的瀏覽器分頁,就會清除該次 Cache 的內容,但是 Full Route Cache 是在伺服器上的全域 Cache,所以不局限於瀏覽器分頁及使用者。另外,要注意如果是使用一般的 <a> 標籤,就不適用於這裡的 Cache 機制。

這裡一樣用一個實際的例子來觀察這裡的 Route Cache 的機制。

在這個例子中,有兩個使用預設 prefetch 行為的 Link,有一個關閉 prefetch 行為的 Link,還有一個使用 a 標籤的連結。

export default function Home() {
  return (
    <div className="flex flex-col font-sans gap-3 items-center justify-items-center min-h-screen p-8 pb-20 sm:p-20">
      <Link className="text-3xl p-2 bg-amber-400" href="/pageA">
        Page A
      </Link>
      <Link className="text-3xl p-2 bg-amber-400" href="/pageB">
        Page B
      </Link>
      <Link
        className="text-3xl p-2 bg-amber-400"
        href="/pageC"
        prefetch={false}
      >
        Page C (with prefetch false)
      </Link>
      <a className="text-3xl p-2 bg-amber-400" href="/a-tag">
        use a tag
      </a>
    </div>
  );
}

https://i.imgur.com/8vaupOO.gif
實測結果可以發現到進入頁面的時候,因為有兩個 Link 沒有關閉 prefetch 行為,所以一開始就有兩個 RSC 的 request,也就是說進到這個頁面時,這兩個 Link 的頁面已經儲存到 Route Cache 中,進入頁面時也就不需要另外發送 request。

而 Page C 的部分因為刻意使用了 prefetch={false},所以在一開始進入頁面時,並沒有先 prefetch 這個 route 對應的 RSC,因此首次進入時,會先發送一個 RSC request,但是第二次進入就不需要再次發送 RSC Request,而是使用 Router Cache 的內容。

另外在 a 標籤的部分,則是因為已經脫離 Client 端的行為了,所以使用 a 標籤轉換頁面時,會視為重新進入頁面,也就會重新發送對應頁面的 HTTP Request,也會導致前面進行的 Router Cache 都被重置。

Page Router 的 Cache 機制!?

看到這裡,我想大家對於 Next.js 的 Cache 機制都有一定程度的了解了,但我們到目前為止都在看 App Router 的 Cache 機制,那 Page Router 的呢?

由於 Page Router 沒有 RSC,也沒有內建的 Data Cache 機制,所以並沒有 App Router 那一套完整的 Cache 機制。雖然 Page Router 沒有這麼完整的 Cache 機制,但實際上還是存在著 Cache 的功能。在 Page Router 的世界中,Page Router 的 Cache 概念主要會是 SSG 和 ISR 先在 build 的階段 build 出來的檔案和 SSR 的 Cache-Control,以及 <Link> 部分的 prefetch。Page Router 雖然沒有 App Router 那麼全面的 Cache 機制,但避免畫面不斷重新渲染的機制還是存在著。

Page Router 沒有 RSC,也沒有 App Router 的 fetch 型 Data Cache。Page Router 的 Cache 機制主要會以 SSG 或 ISR 產出的 HTML 和 JSON、SSR 在 response 上的 Cache-Control,以及 <Link> 的 prefetch 為主。Page Router 雖然沒有 App Router 那麼全面的 Cache 機制,但使用這幾個部分的 Cache 機制,仍然能有效減少伺服器端重渲染。

我踩到的雷到底是被 Cache 在哪裡?

最後再回想一下我在前一篇提到過的畫面會閃一下就圖片的問題是發生在哪裡?我自己目前判斷是一開始命中 Full Route Cache 了,因為實際盤查下來有發現到兩個部分:第一個部分是 response 返回的 HTML 就使用了舊的 API 資料,第二個部分則是在 build 時候,對應的 Route 被標示了 Static。這兩個跡象都指向了 Full Route Cache。

至於為什麼畫面會又更新成最新的圖片,那是因為在 build 版本之後,有透過後台更新了一次圖片,然後對應路由頁面中有包含 Client Component,Client Component 又再次於 Client 端上再打了一次 API,也就導致畫面又更新了一次圖片。

最後處理方式就是讓顯示輪播圖的部分,不在 Server 上就先預渲染,而是改以只在 client 端進行渲染的方式呈現。

總結

最後來一個小小的總結!
在 Next.js 中的 App Router 的 Cache 機制主要會分四個階段:

  1. 先確認對伺服器發送的 HTTP request 是不是有跳過 Full Route Cache 的設定,如果沒有就會使用 Cache 住的整頁內容。
  2. 如果跳過 Full Route Cache,或是有在伺服器上渲染畫面的需求,會檢查這個當次的渲染中,有無相同的 GET Fetch 行為,第一次會正常進行 GET Fetch 取得返回的結果,並且將結果儲存於記憶體,若當次渲染有其他完全相同的 GET Fetch 則不會進行呼叫 API 的動作,會直接返回第一次儲存的 API 結果,這也就是 Request Memoization 的部分。
  3. 在實際打 API 進行 Fetch 的部分,還會有一層 Data Fetch。在這個階段會看 Fetch 有沒有設定 force-cache,如果有設定的話,在第一次打 API 之後,會將返回的結果 Cache 起來,下次再打到這個 API 時,就不會重新打 API,會直截返回 Cache 的結果。
  4. 進入到 Client 端的階段,會將 RSC Payload Cache 起來,這也就是 Route Cache。透過 <Link> 切換頁面之前,如果有 prefetch,就不會在點擊 <Link> 時,再次對伺服器發送 RSC request。

至於 Page Router 的部分主要則是透過 SSG 和 ISR 產出的 HTML 和 JSON、SSR 在 response 上的 Cache-Control,以及 <Link> 的 prefetch 來實行 Cache 的部分。

以上就是 Next.js 的 Cache 機制的內容,雖然 Cache 機制主要是為了要優化網頁的效能,但有時候還是會需要依照實際的情境,去做最適合的搭配,才能讓優化效果發揮到最佳狀態。除此之外,也需要搞清楚每個不同層面的 Cache 機制如何運作,才不會在實作時,踩到不該踩的雷。

參考資料

官方文件 - Data Cache
官方文件 - Client-side Router Cache


上一篇
【Day 13】Next.js 的 Cache 機制 (上) - Full Route Cache & Request Memoization
下一篇
【Day 15】React 的 Router 與 Next.js 的 Router
系列文
從 React 學 Next.js:不只要會用,還要真的懂17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言