跳到內容
返回部落格

2024年10月24日 星期四

我們的快取之旅

作者

前端效能很難做到正確。即使在高度優化的應用程式中,最常見的罪魁禍首仍然是客戶端-伺服器瀑布流。當我們推出 Next.js App Router 時,我們知道我們想要解決這個問題。為了做到這一點,我們需要使用 React Server Components 在單次往返中將客戶端-伺服器 REST 請求移至伺服器端。這意味著伺服器有時必須是動態的,犧牲了 Jamstack 出色的初始載入效能。我們建構了部分預先渲染來解決這種權衡,並同時擁有兩者的優點。

然而,在此過程中,由於我們提供的快取預設值和控制項,開發人員體驗受到了影響。fetch() 的預設值已更改為預設快取以提高效能,但快速原型設計和高度動態的應用程式受到了影響。我們沒有為未使用 fetch() 的本機資料庫存取提供足夠的控制。我們有 unstable_cache(),但它不符合人體工學。這導致需要區段層級的配置,例如 export const dynamic, runtime, fetchCache, dynamicParams, revalidate = ...,作為應急方案。

當然,我們將繼續支援它以實現向後相容性。但現在,我想請您忘記所有這些。我們認為我們有一個更簡單的想法。

我們一直在研究一種新的實驗性模式,它建立在兩個概念之上:<Suspense>use cache

選擇你的冒險

您會注意到的第一件事是,當您將資料新增到元件時,現在會收到錯誤訊息。

app/page.tsx
async function Component() {
  return fetch(...) // error
}
 
export default async function Page() {
  return <Component />
}

若要使用資料、cookies、標頭、目前時間或隨機值,您現在有一個選擇:您希望資料被快取(伺服器端或客戶端)還是每次請求都執行?我以 fetch() 為例,但這適用於任何非同步 Node API,例如資料庫或計時器。

動態

如果您仍在迭代或建構高度動態的儀表板,您可以將元件包裝在 <Suspense> 邊界中。<Suspense> 選擇加入動態資料擷取和串流。

app/page.tsx
async function Component() {
  return fetch(...) // no error
}
 
export default async function Page() {
  return <Suspense fallback="..."><Component /></Suspense>
}

您也可以在根版面配置中執行此操作,或使用 loading.tsx

這可確保您的應用程式外殼保持即時性。您可以繼續在您的 Page 中新增更多資料,因為預設情況下所有資料都將是動態的。預設情況下,不會快取任何內容。不再有隱藏的快取。

靜態

如果您正在建構靜態內容,並且不想使用動態功能,則可以使用新的 use cache 指令。

app/page.tsx
"use cache"
 
export default async function Page() {
  return fetch(...) // no error
}

透過使用 use cache 標記 Page,您表示應快取整個區段。這表示您擷取的任何資料現在都可以快取,從而允許靜態渲染頁面。靜態內容未使用 <Suspense> 邊界。您可以將更多資料新增到頁面,並且所有資料都將被快取。

部分

您也可以混合搭配。例如,您可以將 use cache 放在根版面配置中,以確保它被快取。每個版面配置或頁面都可以獨立快取。

app/layout.tsx
"use cache"
 
export default async function Layout({ children }) {
  const response = await fetch(...)
  const data = await response.json()
  return <html>
    <body>
      <div>{data.notice}</div>
      {children}
    </body>
  </html>
}

同時在特定 Page 中使用動態資料

app/page.tsx
import { Suspense } from 'react'
async function Component() {
  return fetch(...) // no error
}
 
export default async function Page() {
  return <Suspense fallback="..."><Component /></Suspense>
}

快取函式

當使用像這樣的混合方法時,在更靠近 API 呼叫的位置新增快取可能會更方便。

您可以將 use cache 新增到任何非同步函式,就像 use server 一樣。將其視為伺服器動作,但您呼叫的是快取而不是伺服器。它支援超出 JSON 的相同豐富引數和傳回值類型。快取金鑰會自動包含任何引數和閉包,因此您無需手動指定快取金鑰。

app/layout.tsx
async function getNotice() {
  "use cache"
  const response = await fetch(...)
  const data = await response.json()
  return data.notice;
}
 
export default async function Layout({ children }) {
  return <html>
    <body>
      <h1>{await getNotice()}</h1>
      {children}
    </body>
  </html>
}

由於此版面配置中未使用其他資料,因此它可以保持靜態。這種方法的一個好處是,如果您不小心將新的動態資料新增到版面配置中,它將在建置期間觸發錯誤,迫使您做出新的選擇。如果您將 use cache 新增到整個版面配置,它將被快取,而不會產生錯誤。您選擇哪種方法取決於您的使用案例。

標記快取

如果您想透過標籤明確清除快取項目,您可以在 use cache 函式內使用新的 cacheTag() API。

app/utils.ts
import { cacheTag } from 'next/cache';
 
async function getNotice() {
  'use cache';
  cacheTag('my-tag');
}

然後,像以前一樣從伺服器動作中呼叫 revalidateTag('my-tag')

由於可以在資料載入後呼叫此 API,因此您現在可以使用資料來標記您的快取項目。

app/actions.ts
import { unstable_cacheTag as cacheTag } from 'next/cache';
 
async function getBlogPosts(page) {
  'use cache';
  const posts = await fetchPosts(page);
  for (let post of posts) {
    cacheTag('blog-post-' + post.id);
  }
  return posts;
}

定義快取的生命週期

如果您想控制特定項目或頁面在快取中應保留多長時間,可以使用 cacheLife() API

app/page.tsx
"use cache"
import { unstable_cacheLife as cacheLife } from 'next/cache'
 
export default async function Page() {
  cacheLife("minutes")
  return ...
}

預設情況下,它接受以下值

  • "秒"
  • "分鐘"
  • "小時"
  • "天"
  • "週"
  • "max"

選擇最適合您使用案例的大概範圍。無需指定確切的數字並計算一週有多少秒(或毫秒?)。但是,您也可以指定特定值或配置您自己的具名快取設定檔。

除了 revalidate 之外,此 API 還可以控制客戶端快取的 stale 時間以及 expire,後者決定了 Page 如果在一段時間內沒有太多流量時應何時過期。

實驗性功能

這仍然是一個非常實驗性的專案。它尚未準備好用於生產環境,並且仍然缺少功能和錯誤。特別是,我們知道我們需要改進這種新型錯誤的錯誤堆疊。但是,如果您感到躍躍欲試,我們很樂意收到您的早期回饋。

我們將發布更詳細的升級路徑。除了早期錯誤之外,這裡主要的重大變更是否定 fetch() 的預設快取。也就是說,我們建議僅在此早期實驗階段在新專案上進行實驗。如果進展順利,我們希望在次要版本中發布選擇加入版本,並在未來的主要版本中使其成為預設版本。

若要試用它,您必須使用 Next.js 的 canary 版本

npx create-next-app@canary

您還必須在 next.config.ts 中啟用實驗性的 dynamicIO 旗標

next.config.ts
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  experimental: {
    dynamicIO: true,
  }
};
 
export default nextConfig;

在我們的文件中閱讀更多關於 use cachecacheLifecacheTag 的資訊。