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
。
選擇你的冒險
您會注意到的第一件事是,當您將資料新增到元件時,現在會收到錯誤訊息。
async function Component() {
return fetch(...) // error
}
export default async function Page() {
return <Component />
}
若要使用資料、cookies、標頭、目前時間或隨機值,您現在有一個選擇:您希望資料被快取(伺服器端或客戶端)還是每次請求都執行?我以 fetch()
為例,但這適用於任何非同步 Node API,例如資料庫或計時器。
動態
如果您仍在迭代或建構高度動態的儀表板,您可以將元件包裝在 <Suspense>
邊界中。<Suspense>
選擇加入動態資料擷取和串流。
async function Component() {
return fetch(...) // no error
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
您也可以在根版面配置中執行此操作,或使用 loading.tsx
。
這可確保您的應用程式外殼保持即時性。您可以繼續在您的 Page 中新增更多資料,因為預設情況下所有資料都將是動態的。預設情況下,不會快取任何內容。不再有隱藏的快取。
靜態
如果您正在建構靜態內容,並且不想使用動態功能,則可以使用新的 use cache
指令。
"use cache"
export default async function Page() {
return fetch(...) // no error
}
透過使用 use cache
標記 Page,您表示應快取整個區段。這表示您擷取的任何資料現在都可以快取,從而允許靜態渲染頁面。靜態內容未使用 <Suspense>
邊界。您可以將更多資料新增到頁面,並且所有資料都將被快取。
部分
您也可以混合搭配。例如,您可以將 use cache
放在根版面配置中,以確保它被快取。每個版面配置或頁面都可以獨立快取。
"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 中使用動態資料
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 的相同豐富引數和傳回值類型。快取金鑰會自動包含任何引數和閉包,因此您無需手動指定快取金鑰。
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。
import { cacheTag } from 'next/cache';
async function getNotice() {
'use cache';
cacheTag('my-tag');
}
然後,像以前一樣從伺服器動作中呼叫 revalidateTag('my-tag')
。
由於可以在資料載入後呼叫此 API,因此您現在可以使用資料來標記您的快取項目。
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
"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 旗標
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
}
};
export default nextConfig;