Stories

Detail Return Return

Next.js 面試深入剖析緩存機制 - Stories Detail

決勝 Next.js 面試:深入剖析緩存機制

1. 引言:為何 Next.js 中的緩存如此重要?

Next.js 通過其精密的緩存系統,極大地提升了應用程序的性能、降低了服務器負載,並通過提供預先計算的結果或數據來最小化運營成本。
對於任何期望構建優化應用的開發者而言,深入理解這些緩存層是至關重要的。
在技術面試中,對 Next.js 緩存機制的掌握程度,往往能體現出開發者對 Next.js 核心原理的理解深度,以及構建可擴展、高性能 Web 應用的能力。因此,清晰闡述這些概念,將是面試中的一大亮點。

緩存並非 Next.js 中的一個孤立特性,而是其架構設計的基石,尤其是在 App Router 模式下。框架內置了多層次、多樣化的緩存策略,從服務器端的數據獲取到客户端的導航體驗,均有覆蓋。這表明 Next.js 的設計目標是全面控制和優化整個請求-響應生命週期及後續的用户交互,而非僅僅關注局部優化。如果開發者未能充分理解緩存機制,不僅難以完全發揮 Next.js 的性能優勢,在排查性能瓶頸時也會遇到障礙。因此,面試官通常會通過緩存相關問題來考察候選人對 Next.js 底層運作方式的認知。

2. 能否簡要介紹一下 Next.js 中的不同緩存機制?

Next.js 採用了一種多層次的緩存策略,以應對不同場景下的性能優化需求。從高層次來看,這些機制主要包括

  • 請求記憶化 (Request Memoization):在單次服務器端渲染過程中,通過緩存具有相同 URL 和選項的 fetch 請求,優化數據獲取。
  • 數據緩存 (Data Cache):持久化 fetch 請求的結果,使其能夠跨越多個服務器請求乃至多次部署,充當服務器端的數據存儲。
  • 全路由緩存 (Full Route Cache):在服務器端緩存靜態渲染或經過重新驗證的路由的 HTML 和 React 服務器組件負載 (RSC Payload),從而加速響應。
  • 客户端路由器緩存 (Client-side Router Cache):瀏覽器中的一個內存緩存,用於存儲已訪問路由的 RSC Payload,實現更快的客户端導航。

這些緩存層共同構成了一個從服務器數據獲取到客户端導航優化的完整解決方案。這種全面的設計體現了 Next.js 致力於提升整體應用性能的決心。

為了更清晰地對比這些緩存機制,下表總結了它們的核心特性:

Next.js 緩存層概覽

image.png

3. Next.js 中的請求記憶化是什麼?它如何工作,生命週期是怎樣的?”

請求記憶化是 Next.js 對原生 fetch API 的一項擴展,旨在自動記憶化在同一次服務器端渲染過程中具有相同 URL 和選項的請求。

  • 位置:此機制作用於服務器端,具體來説,是在為單個請求渲染 React 組件樹的過程中。
  • 目的:核心目標是避免在單個渲染生命週期內對同一數據進行冗餘的 fetch 調用。例如,當多個組件(可能處於組件樹的不同層級)都需要請求相同的資源時,請求記憶化可以確保實際的網絡請求只發生一次。
  • 生命週期/持續時間:請求記憶化的生命週期與單個服務器請求-響應週期緊密綁定。一旦該請求的 React 組件樹完成渲染,所有記憶化條目都會被清除。這意味着它是一種請求內 (intra-request) 的優化,而非跨請求 (inter-request) 的緩存。
  • 工作原理

    1. 當在渲染路由時首次調用特定的 fetch(url, options) 時,其結果在內存中不存在,這被視為一次緩存 MISS。此時,函數會實際執行,數據將從外部源獲取,並且結果會被存儲在內存中。
    2. 在同一個渲染過程中,如果後續有使用完全相同的 url 和 options 的 fetch 調用,這將是一次緩存 HIT。數據會直接從內存中返回,而不會再次執行函數或發起網絡請求。
  • 重新驗證:由於記憶化數據不跨服務器請求共享,並且僅在渲染期間應用,因此它沒有傳統意義上的“重新驗證”機制。數據在請求結束後即被清除,無需手動干預。
  • 選擇退出:請求記憶化主要適用於 GET 方法的 fetch 請求。其他 HTTP 方法(如 POST、DELETE 等)不會被記憶化。通常不建議選擇退出 GET 請求的記憶化行為,因為它是一項有益的默認優化。雖然可以使用 AbortController 的 signal 屬性來管理單個請求,但這並不會選擇退出記憶化本身。
  • 交互:請求記憶化適用於 generateMetadata、generateStaticParams、佈局 (Layouts)、頁面 (Pages) 以及其他服務器組件 (Server Components) 中的 fetch 請求。然而,它不適用於路由處理器 (Route Handlers),因為路由處理器不屬於 React 組件渲染樹的一部分。

理解請求記憶化的一個關鍵點在於其作用域。它優化的是單次服務器渲染內部的數據獲取,例如,一個父組件和一個子組件在同一次頁面加載過程中都嘗試獲取相同的用户配置數據。
它並不能幫助優化兩個不同用户請求同一頁面時的場景,後者則由數據緩存等其他機制處理。這種自動化的行為簡化了常見場景下的開發,但開發者也需要意識到它的存在,以便準確理解數據流。
例如,如果開發者期望在同一次渲染傳遞中某個 fetch 調用能夠重新執行(比如為了測試某種回退邏輯),他們可能會感到意外,除非他們瞭解請求記憶化的機制。

4. 請解釋 Next.js 的數據緩存。如何管理其持久性和重新驗證(基於時間 vs. 按需)?

數據緩存是 Next.js 內建的一個持久化服務器端緩存層,用於存儲 fetch 請求的結果,這些結果可以在傳入的服務器請求之間共享,甚至可以跨越多次應用部署而保持有效。

  • 位置:服務器端。
  • 目的:核心目的是減少對外部數據源的冗餘請求,從而降低延遲、節約 API 調用成本,並通過提供緩存數據來提升應用性能。
  • 持續時間:默認情況下,數據緩存是持久的,除非被明確重新驗證或通過配置選擇退出緩存。這種跨部署的持久性是一個非常強大的特性,意味着即使在新的代碼版本上線後,先前緩存的有效數據仍可被繼續使用,從而減輕了新部署初期對數據源的衝擊。
  • 工作原理

    1. 當一個 fetch 請求(通常是帶有 cache: 'force-cache' 選項,或者在非動態請求中默認行為)被髮起時,Next.js 會首先檢查數據緩存中是否存在該請求的緩存響應。
    2. 緩存命中 (Cache HIT):如果找到了緩存響應,它會立即返回該響應,並同時將其結果進行請求記憶化,供當前渲染過程中的其他相同請求使用。
    3. 緩存未命中 (Cache MISS):如果未找到緩存響應,則會向實際的數據源發起請求。獲取到的結果不僅會存儲在數據緩存中以供後續請求使用,也會被請求記憶化,用於當前渲染過程。
    4. 對於未被緩存的數據(例如,沒有定義 cache 選項或使用了 { cache: 'no-store' }),結果總是從數據源獲取,但仍然會被請求記憶化,以避免在同一次 React 渲染過程中對相同數據發起重複請求。
  • 重新驗證機制:正確管理數據緩存的生命週期至關重要,Next.js 提供了兩種主要的重新驗證策略:

    • 基於時間的重新驗證 (Time-based Revalidation):

      • 通過在 fetch 請求的 options 對象中使用 next.revalidate 屬性來配置,例如 fetch(url, { next: { revalidate: 60 } }),單位為秒。
      • 這會為資源設置一個緩存生命週期。在指定的時間範圍內(如60秒),任何對該資源的請求都將返回緩存數據。當時間範圍過後,下一個請求到達時,它仍將首先返回緩存中(現在是陳舊的)數據。與此同時,Next.js 會在後台異步觸發數據的重新驗證過程。一旦數據成功獲取,Next.js 將使用新的數據更新數據緩存中的條目。如果後台重新驗證失敗,之前緩存的陳舊數據將繼續被保留和提供。這種行為被稱為 stale-while-revalidate
      • "stale-while-revalidate" 行為是一種刻意的權衡,它優先保證了應用的可用性和感知性能,而不是在短時間內對數據新鮮度的嚴格要求。用户會立即得到響應(儘管可能是舊數據),而無需等待新數據的獲取。這對於非關鍵性數據更新是一種常見的優化模式。
    • 按需重新驗證 (On-demand Revalidation)

      • 允許開發者在特定事件發生後(例如數據更新後)以編程方式精確地使緩存失效。
      • 通過路徑 (revalidatePath(path)):使與特定路由路徑相關聯的數據緩存失效。這通常在 Server Action 或 API 路由中使用。
      • 通過緩存標籤 (revalidateTag(tag)):使與特定緩存標籤相關聯的數據緩存失效。開發者可以在 fetch 請求中通過 next.tags 選項為數據打上標籤,例如 fetch(url, { next: { tags: } })
      • 與基於時間的重新驗證不同,按需重新驗證會直接清除緩存中的相應條目。這意味着下一次對該數據的請求將是緩存 MISS,必須從數據源重新獲取新數據並存入緩存,而不是先提供陳舊數據。
      • 緩存標籤 (revalidateTag) 相較於基於路徑 (revalidatePath) 的重新驗證,提供了一種更細粒度且解耦的緩存管理方式,尤其適用於在多個路由間共享的數據。例如,如果一個產品詳情數據同時展示在產品頁、特色產品列表和用户願望清單中,通過路徑更新會非常繁瑣。而使用標籤(如 product-123),只需在產品數據更新後重新驗證該標籤,所有使用了此標籤的 fetch 調用(及其在數據緩存中的條目)都會失效,無論它們位於哪個頁面。
  • 選擇退出

    • 可以通過在 fetch 請求中設置 { cache: 'no-store' } 來讓特定的響應不進入數據緩存。這意味着每次請求都會從數據源獲取最新數據(但請注意,該次請求的結果在當前渲染週期內仍會被請求記憶化。

數據緩存是 Next.js 實現高性能動態內容服務和降低外部 API 調用頻率的核心。其跨部署的持久性特性,結合靈活的重新驗證策略,使得開發者能夠精細地平衡數據新鮮度與應用性能。

5. 全路由緩存存儲了什麼?靜態渲染與動態渲染如何影響它?

全路由緩存是 Next.js 在服務器端實現的一項重要優化,它存儲了路由的完整渲染輸出,具體包括 React 服務器組件負載 (RSC Payload) 和最終生成的 HTML。

  • 位置:服務器端。
  • 目的:通過直接提供預先渲染好的路由內容,而不是在每次請求時都重新執行渲染邏輯,從而顯著減少渲染成本,提升應用性能,加快響應速度。
  • 持續時間:對於靜態渲染的路由,全路由緩存默認是持久的。然而,與數據緩存不同,全路由緩存會在應用進行新的部署時被清除。
  • 工作原理(渲染流程)

    1. React 在服務器端渲染:Next.js 利用 React 的 API 來協調渲染過程。渲染工作被分解為多個塊,通常是按單個路由段 (route segment) 和 Suspense 邊界劃分。每個塊的渲染分兩步進行:

      • React 將服務器組件渲染成一種特殊的數據格式,這種格式經過優化,適合流式傳輸,稱為 React 服務器組件負載 (RSC Payload)
      • Next.js 使用 RSC Payload 和客户端組件的 JavaScript 指令,在服務器上進一步渲染生成最終的 HTML
    2. Next.js 在服務器端緩存 (全路由緩存):Next.js 的默認行為是在服務器上緩存上述渲染結果(即 RSC Payload 和 HTML)。這適用於在構建時靜態渲染的路由,或在重新驗證期間更新的路由。
    3. React 在客户端進行水合 (Hydration) 與協調:當用户請求一個路由時,在客户端:

      • 服務器返回的 HTML 用於立即顯示頁面的快速、非交互式初始預覽,這包括服務器組件和客户端組件的靜態部分。
      • RSC Payload 用於在客户端協調(reconcile)已渲染的服務器組件樹,並更新 DOM 結構。
      • JavaScript 指令則用於水合客户端組件,使其具備交互能力。
  • 靜態渲染 vs. 動態渲染

    • 靜態路由 (Static Routes):在構建時(或在後續的重新驗證過程中)進行渲染,其輸出(HTML 和 RSC Payload)默認存儲在全路由緩存中。這些路由的響應速度非常快。全路由緩存的這種機制,使得純靜態內容的路由可以獲得極致的性能。
    • 動態路由 (Dynamic Routes):如果路由中使用了動態函數(如 cookies()、headers()),或者其包含的 fetch 請求配置為 cache: 'no-store' 或 revalidate: 0,那麼該路由將在請求時按需渲染。這類動態渲染的路由的輸出不會被存儲在全路由緩存中。
  • 失效機制

    • 數據重新驗證:這是全路由緩存更新的關鍵驅動因素。當數據緩存中的某個條目被重新驗證(例如,通過 revalidateTag 或 fetch 中的 next.revalidate 選項),並且該數據被某個路由使用時,Next.js 會在服務器上重新渲染該路由。新生成的 RSC Payload 和 HTML 會替換掉全路由緩存中舊的條目。這種緊密的耦合關係確保了當底層數據發生變化時,用户最終看到的頁面內容也能得到更新。
    • 重新部署:當應用的新版本被部署時,整個全路由緩存會被清除。這意味着新部署後,所有路由(即使是靜態的)在首次被請求時都需要重新渲染(或從構建產物中讀取)並填充緩存。
  • 選擇退出(強制動態渲染)

    • 在服務器組件中使用動態 API,如 cookies()、headers() 或讀取 searchParams。
    • 使用路由段配置選項 export const dynamic \= 'force-dynamic' 或 export const revalidate \= 0。這會跳過全路由緩存和數據緩存,組件將在每次服務器請求時重新渲染並獲取數據。不過,客户端的路由器緩存仍然適用。
    • 如果路由內的任何一個 fetch 請求選擇退出了數據緩存(即使用了 { cache: 'no-store' }),那麼整個路由將變為動態渲染,並因此選擇退出全路由緩存。該特定 fetch 請求的數據會在每次請求時重新獲取,而其他未選擇退出緩存的 fetch 請求仍可使用數據緩存。這允許在同一路由中混合使用緩存和非緩存數據。 一個重要的影響是,即使路由中只有一小部分內容依賴於動態函數或不可緩存的數據,整個路由的輸出(HTML 和 RSC Payload)都無法在全路由緩存中被有效地緩存以供後續請求使用。開發者需要意識到這種“動態性傳染”效應,因為它可能因意外引入動態函數(例如,不必要地讀取 cookie)而導致整個路由失去全路由緩存的性能優勢。

全路由緩存的填充時機也值得注意:它不僅在構建時為真正的靜態路由填充,也可以為配置了重新驗證策略的路由按需填充。這使得路由不單純是“靜態”或“動態”的二元狀態,而是可以存在於一個連續的光譜上,提供了類似以往 Pages Router 中 ISR (增量靜態再生) 的靈活性。

6. 請描述客户端路由器緩存。它的用途、持續時間和失效方式是怎樣的?

客户端路由器緩存是 Next.js 在用户瀏覽器中實現的一個內存緩存機制,它專門存儲已訪問過或已預取的路由段的 React 服務器組件負載 (RSC Payloads)。

  • 位置:客户端(瀏覽器內存中。
  • 目的:主要目的是提升客户端導航的性能和用户體驗。通過緩存 RSC Payload,當用户導航到已訪問或預取的路由時,應用可以幾乎即時地完成頁面過渡,無需為獲取 RSC Payload 而發起新的服務器請求。它也為即時的前進/後退瀏覽器導航提供了支持。此緩存的核心價值在於優化用户在應用內部導航時的感知速度。
  • 存儲內容:它存儲的是 RSC Payload,並且這些負載是按單個路由段(如佈局 layout.js、加載狀態 loading.js 和頁面 page.js)進行劃分和存儲的。這種細粒度的存儲方式使得 Next.js App Router 能夠在導航時高效地更新頁面部分內容,而不是進行完整的頁面重載。
  • 持續時間

    • 會話級別:緩存條目在同一次瀏覽器會話(例如,同一個標籤頁)內的導航中是持久的。但是,當用户執行完整的頁面刷新操作時,整個客户端路由器緩存會被清除。
    • 自動失效期:緩存的路由段(特別是佈局和加載狀態)會在特定時間後自動失效。從 Next.js 15 開始,頁面段 (page.js) 默認選擇退出預取。當使用 \<Link prefetch={true}\> (或 router.prefetch) 顯式預取時,靜態路由預取內容的緩存時間通常為30秒,動態路由則為5分鐘。佈局和加載狀態通常有更長的緩存時間。
  • 失效機制

    • Server Actions (服務器操作)

      • 在 Server Action 中使用 revalidatePath() 或 revalidateTag() 不僅會使服務器端的數據緩存(以及間接影響的全路由緩存)失效,還會同時使客户端路由器緩存中相應的條目失效。這是確保數據在整個應用棧中保持一致性的關鍵機制。
      • 在 Server Action 中修改 Cookies(通過 cookies.set() 或 cookies.delete())也會導致客户端路由器緩存失效。這樣做是為了防止因 Cookie 值(例如,與身份驗證狀態相關的 Cookie)的改變而導致用户界面顯示陳舊信息。 Server Actions 在此扮演了重要角色,它們提供了一條從數據變更(通常由 Server Action 觸發)到客户端 UI 更新的直接路徑,從而保持緩存的同步。
    • router.refresh():在客户端調用 router.refresh() API 會使當前路由的客户端路由器緩存失效,並向服務器發起新的請求以獲取最新的 RSC Payload。
    • 頁面刷新:用户執行完整的瀏覽器頁面刷新會清除整個客户端路由器緩存。
  • 選擇退出(預取行為)

    • 如前所述,從 Next.js 15 開始,頁面段默認選擇退出預取。
    • 可以通過將 <Link> 組件的 prefetch 屬性設置為 false 來禁用對特定鏈接目標的預取行為。

客户端路由器緩存專門存儲 RSC Payload 而非完整的 HTML,這一點對於理解 App Router 的工作方式至關重要。初始頁面加載時,客户端接收 HTML。對於後續的客户端導航,如果目標路由的 RSC Payload 存在於路由器緩存中,Next.js 就可以利用這個 Payload 來協調新的服務器組件樹與現有的客户端組件樹,並高效地更新 DOM,而無需重新請求完整的 HTML這正是 App Router 實現平滑、快速頁面轉換的核心機制之一。

7. 交互與高級概念

理解各個緩存層如何獨立運作固然重要,但更關鍵的是掌握它們之間以及與 Next.js 其他核心部分(如服務器組件、客户端組件和路由處理器)的交互方式。

  • 服務器組件、客户端組件及路由處理器與緩存的交互

    • 服務器組件 (Server Components)

      • 可以直接使用 fetch API,這將自動與請求記憶化和數據緩存機制交互。
      • 它們的渲染輸出,即 RSC Payload,是構成全路由緩存和客户端路由器緩存內容的核心部分。
      • 可以通過使用動態函數(如 cookies()、headers())或配置路由段的 dynamic、revalidate 選項來影響全路由緩存的行為。
    • 客户端組件 (Client Components)

      • 由於它們在客户端渲染,因此不直接參與服務器端的緩存(如數據緩存、全路由緩存)的構建過程。
      • 它們的 JavaScript 指令是初始服務器渲染輸出的一部分,用於客户端的水合過程。
      • 可以通過 <Link> 組件或 router.push() 等方式觸發導航,這些導航會利用客户端路由器緩存。
      • 可以調用 Server Actions。這些 Server Actions 自身在服務器端執行,並能夠反過來使服務器端緩存(數據緩存、全路由緩存)和客户端路由器緩存失效。
      • 可以使用 router.refresh() API 來主動使當前路由的客户端路由器緩存失效。
    • 路由處理器 (Route Handlers, 例如 API 路由)

      • 如果在路由處理器內部使用 fetch,那麼這些 fetch 調用可以與數據緩存交互(例如,緩存 GET 請求的結果)。
      • 然而,請求記憶化機制不適用於路由處理器內部的 fetch 調用,因為路由處理器不屬於 React 組件樹的渲染生命週期。
      • 路由處理器不直接產生 RSC Payload,因此它們不像頁面那樣直接構成全路由緩存或客户端路由器緩存的內容。但是,它們提供的數據可能被服務器組件獲取,從而間接影響被緩存的內容。
      • 一個重要的區別是:在路由處理器中(例如,在一個 POST 請求處理器中通過 revalidateTag)重新驗證數據緩存,並不會像在 Server Action 中那樣自動使客户端路由器緩存失效。這使得 Server Actions 在維護跨層緩存一致性方面更為強大。

        - 緩存間的聯動效應:一個緩存的重新驗證如何影響其他緩存?

    • 數據緩存 > 全路由緩存:這是一個至關重要的交互。當數據緩存中的某個條目(例如,通過 fetch 的 next.revalidate 選項、revalidateTag 或 revalidatePath)被重新驗證,並且該數據被某個路由使用時,Next.js 會在該路由下一次被請求時(或對於基於時間的重新驗證,在後台)重新渲染該路由。新生成的 RSC Payload 和 HTML 將更新全路由緩存中的對應條目。
    • 數據緩存 > 客户端路由器緩存 (通過 Server Actions):當在 Server Action 中使用 revalidateTag 或 revalidatePath 時,它不僅會使數據緩存失效(並因此可能觸發全路由緩存的更新),還會通知客户端使其路由器緩存中的相關段失效。這確保了客户端在下次導航或調用 router.refresh() 後會獲取最新的 RSC Payload。
    • 選擇退出數據緩存 > 全路由緩存:如果一個路由內的某個 fetch 請求通過 { cache: 'no-store' } 選擇退出了數據緩存,那麼整個路由將變為動態渲染,並因此選擇退出全路由緩存。該路由將在每次請求時都重新渲染。
    • 全路由緩存的失效不影響數據緩存:反之則不成立。使全路由緩存失效或選擇退出全路由緩存(例如,通過將路由配置為動態渲染)並不會影響數據緩存。即使路由本身是動態渲染的,其中由 fetch 獲取的數據仍然可以被緩存到數據緩存中。 這種失效傳播存在明顯的層級關係:數據緩存的變動可以傳遞到全路由緩存,再通過特定機制(主要是 Server Actions)傳遞到客户端路由器緩存。反向的自動影響通常不存在。
  • 動態函數(cookies(), headers(), searchParams)對緩存的影響

    • 當服務器組件使用諸如 cookies()、headers()(來自 next/headers)或 searchParams(作為頁面 props 傳入)等動態函數時,Next.js 會檢測到這種動態性。
    • 這種使用會自動使包含這些動態函數的路由選擇退出全路由緩存。結果是,該路由將在請求時進行動態渲染。
    • 即便如此,該路由內部的 fetch 請求仍然可以使用數據緩存,除非這些特定的 fetch 請求也明確選擇了退出(例如,通過 cache: 'no-store')1。
    • 此外,在 Server Action 中使用 cookies.set() 或 cookies.delete() 還會導致客户端路由器緩存失效。 這種“動態性傳染”效應意味着,即使一個路由的大部分內容是靜態的,只要其中一個組件使用了動態函數或依賴於不可緩存的數據獲取,整個路由的輸出就無法在全路由緩存層面被緩存。開發者應審慎使用動態函數,如果希望最大化利用全路由緩存,可能需要將動態部分隔離到特定的組件或子路由中,或者考慮使用客户端組件來處理高度動態和個性化的內容。

8. 配置和控制緩存行為

Next.js 提供了多種 API 和配置選項,允許開發者對不同緩存層的行為進行細緻的控制。掌握這些工具是有效利用 Next.js 緩存能力的關鍵。

  • fetch API 選項 (對數據進行最細粒度的控制):

    • cache: 'force-cache':(默認行為,除非在動態上下文中) 指示 fetch 在數據緩存中查找匹配項。如果未命中,則獲取數據並存入緩存。
    • cache: 'no-store':每次請求都從數據源獲取數據,不存入數據緩存(但結果在當前渲染過程中仍會被請求記憶化)。此選項會導致使用該 fetch 的路由選擇退出全路由緩存。
    • next.revalidate: <seconds>:為數據緩存中的條目設置基於時間的重新驗證策略。使用此選項的路由仍然可以是靜態生成的,並利用全路由緩存,其內容會週期性地在後台更新。
    • next.tags: ['tag1', 'tag2']:為 fetch 請求在數據緩存中的條目添加一個或多個標籤,以便後續通過 revalidateTag() 進行按需重新驗證。
  • 路由段配置選項 (在路由級別進行更廣泛的控制):這些選項通常在頁面或佈局文件中導出。
  • export const dynamic = 'auto':(默認值) Next.js 會嘗試儘可能多地緩存內容,但如果檢測到動態函數的使用或不可緩存的數據獲取,則會自動選擇動態渲染該路由段
  • export const dynamic = 'force-dynamic':強制對該路由段進行動態渲染。這將跳過全路由緩存,並且該段內的 fetch 請求也會默認跳過數據緩存(行為類似於 revalidate: 0 且所有 fetch 都使用 cache: 'no-store')。客户端路由器緩存仍然適用
  • export const dynamic = 'error':強制進行靜態渲染。如果在此路由段中檢測到動態函數或不可緩存的數據獲取,則會拋出錯誤
  • export const dynamic = 'force-static':強制進行靜態渲染。動態函數會被視為返回空值或默認值
  • export const revalidate = <seconds>:為該路由段設置一個重新驗證間隔(影響全路由緩存以及段內 fetch 請求的數據緩存)。其行為類似於 Pages Router 中的 ISR (增量靜態再生)。若設置 revalidate = 0,則等同於強制動態渲染 (dynamic = 'force-dynamic')
  • export const fetchCache:(高級選項) 更細緻地控制整個路由段內 fetch 請求的默認緩存行為,例如 'default', 'only-cache', 'force-no-store' 等
  • 按需重新驗證函數

    • revalidateTag(tag) 和 revalidatePath(path):這兩個函數用於按需觸發緩存失效,通常在 Server Actions 或 API 路由處理器(例如,響應數據變更的 POST 請求)中使用。
  • 客户端導航與刷新

    • <Link prefetch={boolean | undefined}>:控制客户端對鏈接目標進行預取的行為,這會影響客户端路由器緩存的填充。從 Next.js 15 開始,頁面段默認不進行預取。
    • router.refresh():客户端 API,用於使當前路由的客户端路由器緩存失效,並從服務器重新獲取最新的 RSC Payload。

Next.js 的緩存配置提供了一個從非常細緻(單個 fetch 調用)到較為寬泛(整個路由段)的控制範圍。這種分層的方法允許開發者在路由級別應用通用的緩存策略,然後在需要時針對特定的數據獲取進行微調或覆蓋。理解這一點非常重要:Next.js 的默認行為(例如 dynamic = 'auto' 和 fetch 隱式使用 force-cache)是傾向於積極緩存的。

因此,開發者必須主動選擇退出緩存,當處理高度動態、個性化或敏感數據,以防止提供陳舊或不正確的信息時,這一點尤為關鍵。

為了便於記憶和查閲,下表總結了關鍵 API 及其對緩存的影響:

關鍵 API 及其對緩存的影響

image.png
image.png
image.png

9. 超越 fetch:React.cache 和 unstable_cache

雖然 Next.js 對 fetch API 的緩存能力進行了深度集成和優化,但在實際開發中,並非所有數據獲取都通過 fetch 完成。例如,項目可能使用特定的數據庫客户端、CMS SDK 或不基於 fetch 的 GraphQL 客户端。為了將這些非 fetch 的數據源也納入 Next.js 的緩存體系,框架提供了 React.cache 和 unstable_cache 這兩個工具。

  • React.cache 函數

    • Next.js 中的 fetch API 會自動處理請求記憶化(通過 React 的緩存機制)和持久化緩存(通過數據緩存)。
    • 然而,如果使用的是不依賴 fetch 的數據獲取庫,那麼 fetch 自帶的自動記憶化功能將無法生效。
    • React.cache 是 React 提供的一個函數,它允許開發者包裹一個函數,並記憶化其返回值。通過用 React.cache 包裹那些來自其他庫的數據獲取調用,可以實現在單次服務器渲染傳遞中類似請求記憶化的效果,避免對同一非 fetch 數據源的重複調用。
    • 需要明確的是,React.cache 僅提供記憶化功能(類似於 fetch 的請求記憶化),它不提供跨請求的持久化緩存,這一點與數據緩存有着本質區別 。
  • unstable_cache 函數

    • 對於那些不使用 fetch 進行數據獲取,並且需要實現跨請求的持久化緩存(類似於數據緩存提供的能力)的場景,Next.js 提供了 unstable_cache 函數。
    • 這個函數允許緩存任何函數的執行結果,而不僅僅是 fetch 請求的結果。
    • 開發者可以為其提供緩存標籤和重新驗證選項(如時間或按需),其功能與 fetch 結合數據緩存時的配置相似。
    • unstable_cache 實際上是將數據緩存的強大能力擴展到了非 fetch 的數據源。
  • 何時使用

    • 當需要在服務器的單次渲染過程中對來自非 fetch 數據源的調用進行去重時,應使用 React.cache。
    • 當需要將來自非 fetch 數據源的結果進行跨多個請求和部署的持久化緩存,並且需要重新驗證能力時,應使用 unstable_cache。

React.cache 和 unstable_cache 是非常重要的補充工具,它們彌合了 Next.js 內建 fetch 優化與多樣化數據獲取方式之間的差距。React.cache 在概念上與請求記憶化對齊,而 unstable_cache 則與數據緩存對齊,為開發者提供了在不同場景下一致的緩存思維模型。值得注意的是,unstable_cache 名稱中的 unstable\_ 前綴表明其 API 可能會在未來的 Next.js 版本中發生變化,開發者在使用時應關注官方文檔的更新,並意識到其潛在的維護成本。

10. 總結:Next.js 緩存面試的關鍵要點

在準備關於 Next.js 緩存的面試時,不僅要理解每個緩存層的孤立功能,更要展現出對其整體協作方式、配置選項以及背後設計哲學的深刻認知。

核心回顧點:

  • 多層系統:清晰闡述 Next.js 的四個主要緩存層:請求記憶化、數據緩存、全路由緩存和客户端路由器緩存。
  • 核心屬性:對每一層,都能説明其用途、位置(服務器端/客户端)、默認持續時間以及主要的失效/重新驗證機制。
  • fetch 的中心地位:強調 fetch API 及其選項(cache, next.revalidate, next.tags)在控制服務器端緩存(數據緩存、間接影響全路由緩存)中的核心作用。
  • 渲染模式與緩存:區分靜態生成(構建時緩存)、動態渲染(請求時渲染)以及各種重新驗證策略(基於時間、按需)對緩存行為的影響。
  • 動態性的影響:理解動態函數(如 cookies(), headers())和未緩存的數據獲取(cache: 'no-store')如何導致路由選擇退出全路由緩存。
  • Server Actions 的角色:認識到 Server Actions 在執行數據變更和同時使多個相關緩存層(數據緩存、全路由緩存、客户端路由器緩存)失效方面的強大能力。
  • 非 fetch 數據的緩存:瞭解何時以及如何使用 React.cache(用於單次渲染的記憶化)和 unstable_cache(用於持久化緩存)。
  • 關注“為什麼”:不僅要描述“是什麼”和“怎麼做”,更要解釋“為什麼”這麼設計——即每個緩存機制如何為提升性能、降低成本和改善用户體驗做出貢獻。

常見的理解誤區或面試考察點:

  • 混淆請求記憶化(作用於單次請求生命週期內)與數據緩存(持久化跨請求)。
  • 未能意識到單個動態函數的使用或一個 cache: 'no-store' 的 fetch 調用就可能使整個路由變為動態渲染,從而失去全路由緩存的優勢。
  • 忘記全路由緩存會在應用重新部署時清除,而數據緩存(如果配置得當)可以跨部署持久存在。
  • 對 stale-while-revalidate(基於時間的重新驗證)與按需重新驗證(清除緩存,下次請求強制重新獲取)的行為差異理解不清。
  • 不清楚在數據發生變更後如何正確地使相關緩存失效,這可能導致用户看到陳舊的內容。

最終,其實我們希望看到的是候選人能夠將這些獨立的緩存知識點聯繫起來,形成一個關於 Next.js 如何通過緩存實現高性能應用的完整圖景。能夠清晰地闡述一個數據變更(例如,通過 Server Action 更新了數據庫中的一條記錄)如何通過一系列緩存失效和重新驗證機制,最終反映到用户界面上,將充分展示候選人對 Next.js 緩存系統及其交互的深入理解。
image.png

user avatar binghe001 Avatar chongdongdedaxiongmao_kxfei Avatar yizhidanshendetielian Avatar chenxiaoxi_619df8932f34a Avatar dabaibai_5ebb89514c34a Avatar wei4118268_5df1ce05d1877 Avatar emma_5aa5f6ff6ec8d Avatar paul2015 Avatar zxc7563598 Avatar 5si66p3e Avatar sucre_199782 Avatar aion_6356676d25766 Avatar
Favorites 15 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.