如何實作增量靜態再生 (ISR)
增量靜態再生 (ISR) 讓您能夠:
- 更新靜態內容而無需重建整個網站
- 透過提供預渲染的靜態頁面來減少伺服器負載
- 確保自動為頁面添加正確的
cache-control
標頭 - 處理大量內容頁面而無需長時間的
next build
以下是一個最小範例:
interface Post {
id: string
title: string
content: string
}
// Next.js 會在請求進來時使快取失效
// 最多每 60 秒一次
export const revalidate = 60
// 我們只會在建置時預渲染來自 `generateStaticParams` 的參數
// 如果請求的路徑尚未生成
// Next.js 會按需伺服器渲染該頁面
export const dynamicParams = true // 或設為 false,以在未知路徑回傳 404
export async function generateStaticParams() {
const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return posts.map((post) => ({
id: String(post.id),
}))
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then(
(res) => res.json()
)
return (
<main>
<h1>{post.title}</h1>
<p>{post.content}</p>
</main>
)
}
// Next.js 會在請求進來時使快取失效
// 最多每 60 秒一次
export const revalidate = 60
// 我們只會在建置時預渲染來自 `generateStaticParams` 的參數
// 如果請求的路徑尚未生成
// Next.js 會按需伺服器渲染該頁面
export const dynamicParams = true // 或設為 false,以在未知路徑回傳 404
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return posts.map((post) => ({
id: String(post.id),
}))
}
export default async function Page({ params }) {
const { id } = await params
const post = await fetch(`https://api.vercel.app/blog/${id}`).then((res) =>
res.json()
)
return (
<main>
<h1>{post.title}</h1>
<p>{post.content}</p>
</main>
)
}
這個範例的運作方式如下:
- 在
next build
期間,所有已知的部落格文章會被生成(此範例中有 25 篇) - 對這些頁面的所有請求(例如
/blog/1
)會被快取且立即回應 - 60 秒過後,下一個請求仍會顯示快取的(過期)頁面
- 快取失效後,新版本的頁面會在背景開始生成
- 成功生成後,Next.js 會顯示並快取更新後的頁面
- 如果請求
/blog/26
,Next.js 會按需生成並快取此頁面
參考資料
路由區段配置
函式
範例
基於時間的重新驗證
這會在 /blog
上取得並顯示部落格文章列表。一小時後,此頁面的快取會在下次訪問時失效。然後,在背景中,會使用最新的部落格文章生成頁面的新版本。
interface Post {
id: string
title: string
content: string
}
export const revalidate = 3600 // 每小時失效一次
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts: Post[] = await data.json()
return (
<main>
<h1>部落格文章</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
)
}
export const revalidate = 3600 // 每小時失效一次
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<main>
<h1>部落格文章</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
)
}
我們建議設定較高的重新驗證時間。例如,1 小時而非 1 秒。如果您需要更精確的控制,請考慮使用按需重新驗證。如果您需要即時資料,請考慮切換到動態渲染。
使用 revalidatePath
進行按需重新驗證
對於更精確的重新驗證方法,可以使用 revalidatePath
函式按需使頁面失效。
例如,這個伺服器動作會在新增文章後被呼叫。無論您在伺服器元件中使用 fetch
或連接到資料庫來取得資料,這都會清除整個路由的快取,並允許伺服器元件取得最新資料。
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
// 使快取中的 /posts 路由失效
revalidatePath('/posts')
}
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
// 使快取中的 /posts 路由失效
revalidatePath('/posts')
}
使用 revalidateTag
進行按需重新驗證
對於大多數使用情境,建議使整個路徑失效。如果您需要更細粒度的控制,可以使用 revalidateTag
函式。例如,您可以標記個別的 fetch
呼叫:
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog', {
next: { tags: ['posts'] },
})
const posts = await data.json()
// ...
}
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog', {
next: { tags: ['posts'] },
})
const posts = await data.json()
// ...
}
如果您使用 ORM 或連接到資料庫,可以使用 unstable_cache
:
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getCachedPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
export default async function Page() {
const posts = getCachedPosts()
// ...
}
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
const getCachedPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)
export default async function Page() {
const posts = getCachedPosts()
// ...
}
然後您可以在伺服器動作或路由處理器中使用 revalidateTag
:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
// 使快取中標記為 'posts' 的所有資料失效
revalidateTag('posts')
}
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
// 使快取中標記為 'posts' 的所有資料失效
revalidateTag('posts')
}
處理未捕獲的例外
如果在嘗試重新驗證資料時拋出錯誤,則會繼續從快取中提供最後成功生成的資料。在下一個後續請求中,Next.js 會重試重新驗證資料。了解更多關於錯誤處理的資訊。
自訂快取位置
如果您想將快取的頁面和資料持久化到持久儲存,或者在 Next.js 應用程式的多個容器或實例之間共享快取,可以配置 Next.js 快取位置。了解更多。
疑難排解
在本地開發中除錯快取資料
如果您使用 fetch
API,可以添加額外的日誌記錄來了解哪些請求被快取或未快取。了解更多關於 logging
選項的資訊。
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
}
驗證正確的生產環境行為
若要驗證您的頁面在生產環境中是否正確快取及重新驗證,可以在本地執行 next build
後再執行 next start
來運行 Next.js 的生產環境伺服器。
這將讓您測試 ISR (Incremental Static Regeneration) 行為在生產環境中的運作方式。如需進一步除錯,請在 .env
檔案中加入以下環境變數:
NEXT_PRIVATE_DEBUG_CACHE=1
這會讓 Next.js 伺服器在主控台記錄 ISR 快取命中與未命中的情況。您可以檢查輸出內容,查看哪些頁面是在 next build
時生成的,以及當路徑被按需存取時頁面是如何更新的。
注意事項
- ISR 僅在使用 Node.js 運行環境 (預設) 時支援。
- 建立 靜態匯出 (Static Export) 時不支援 ISR。
- 如果在靜態渲染的路由中有多個
fetch
請求,且每個請求有不同的revalidate
頻率,ISR 將使用最短的時間。然而,這些重新驗證頻率仍會受到 資料快取 (Data Cache) 的尊重。 - 如果路由中使用的任何
fetch
請求的revalidate
時間為0
,或明確設定為no-store
,該路由將被 動態渲染 (dynamically rendered)。 - 中介軟體 (Middleware) 不會在按需 ISR 請求時執行,這意味著任何路徑重寫或中介軟體中的邏輯都不會生效。請確保您重新驗證的是確切路徑。例如
/post/1
而非重寫後的/post-1
。
平台支援
部署選項 | 是否支援 |
---|---|
Node.js 伺服器 | 是 |
Docker 容器 | 是 |
靜態匯出 | 否 |
轉接器 (Adapters) | 依平台而定 |
了解如何在使用 Next.js 自架時 設定 ISR。
版本歷史
版本 | 變更 |
---|---|
v14.1.0 | 自訂 cacheHandler 功能穩定。 |
v13.0.0 | 引入 App Router。 |
v12.2.0 | Pages Router: 按需 ISR 功能穩定。 |
v12.0.0 | Pages Router: 新增 Bot-aware ISR 回退機制。 |
v9.5.0 | Pages Router: 引入穩定的 ISR 功能。 |