Next.js 的 revalidate 跟 force-static 搭在一起到底會發生什麼事?
2025-05-202.52Front EndNextjsReact
最近在玩 Next.js 的時候,發現 export const dynamic = 'force-static'
搭配 revalidate
系列功能會有一些… 意料之外的行為,所以這邊稍微記錄一下,順便整理幾個可能會搞混的場景。
基礎場景設計
我們有以下幾個重要的檔案與設定:
📁 根目錄
├── 📁 components
│ ├── 📄 btns.tsx
├── 📁 utils
│ ├── 📄 getRandomNumber.ts
├── 📁 app
│ ├── 📁 qq
│ │ ├── 📄 page.tsx
│ ├── 📄 page.tsx
page.tsx
(Home 頁面)
import Btnsfrom '@/app/_components/btns' import { getRandomNumber } from '@/utils/getRandomNumber' export const dynamic = 'force-static' export async function generateMetadata() { const randomNumber = await getRandomNumber() return { title: `Home - ${randomNumber}`, } } export default function Home() { return ( <> <Btns /> </> ) }
這是首頁的 component,透過 generateMetadata()
動態生成標題,但由於設了 force-static
,會在 build 時輸出 HTML,並套用 full route cache。
/qq/page.tsx
(次要頁面)
import React from 'react' import { getRandomNumber } from '../getRandomNumber' export const dynamic = 'force-static' export async function generateMetadata() { const randomNumber = await getRandomNumber() return { title: `Home - ${randomNumber}`, } } export default async function Page() { const randomNumber = await getRandomNumber() return <div>Page - {randomNumber}</div> }
qq 是一個子路由,用來測試子路由對應情境
getRandomNumber
(模擬 Server 變動資料)
import { cache } from 'react' import { unstable_cache } from 'next/cache' function getRandomNumber() { return new Promise<number>((resolve) => { setTimeout(() => { const randomNumber = Math.random() resolve(randomNumber) }, 1000) }) } const cachedRandomNumber = unstable_cache(cache(getRandomNumber), [ 'random-number', ]) export { cachedRandomNumber as getRandomNumber }
我使用了 unstable_cache
與 cache
將隨機數據標籤為 random-number
,代表資料本身是可被 tag revalidation 的。
注意 : 如果 unstable_cache revalidate 參數沒有帶,則資料永遠 cache,直到使用
revalidatePath
或revalidateTag
可參考 官方文檔
Server Actions
// lib/actions.ts 'use server' import { revalidatePath, revalidateTag } from 'next/cache' /** * Revalidates multiple paths in Next.js * @param paths Array of paths to revalidate * @returns Object containing success status and message */ export async function revalidateMultiplePaths( paths: string[], type?: 'page' | 'layout' ) { try { if (!Array.isArray(paths) || paths.length === 0) { return { success: false, message: 'No paths provided for revalidation', } } // Revalidate each path paths.forEach((path) => { // Ensure path starts with a slash const normalizedPath = path.startsWith('/') ? path : `/${path}` revalidatePath(normalizedPath, type) }) return { success: true, message: `Successfully revalidated ${paths.length} paths`, paths, } } catch (error) { console.error('Error revalidating paths:', error) return { success: false, message: error instanceof Error ? error.message : 'Unknown error occurred', error, } } } /** * Revalidates multiple tags in Next.js * @param tags Array of tags to revalidate * @returns Object containing success status and message */ export async function revalidateMultipleTags(tags: string[]) { try { if (!Array.isArray(tags) || tags.length === 0) { return { success: false, message: 'No tags provided for revalidation', } } // Revalidate each tag tags.forEach((tag) => { revalidateTag(tag) }) return { success: true, message: `Successfully revalidated ${tags.length} tags`, tags, } } catch (error) { console.error('Error revalidating tags:', error) return { success: false, message: error instanceof Error ? error.message : 'Unknown error occurred', error, } } }
我定義兩個 server action 分別處理 路由
和 Data Tag
刷新
Btns 元件
'use client' import { useRouter } from 'next/navigation' import { revalidateMultiplePaths, revalidateMultipleTags } from '@/action' import { Button } from '@/components/ui/button' export default function Btns() { const router = useRouter() return ( <section className="section-container py-10 md:py-15"> <Button onClick={() => { revalidateMultiplePaths(['/']) router.refresh() }} > revalidate / with type page </Button> <Button onClick={() => { revalidateMultiplePaths(['/'], 'layout') router.refresh() }} > revalidate / with type layout </Button> <Button onClick={() => { revalidateMultiplePaths(['/qq']) router.refresh() }} > revalidate /qq </Button> <Button onClick={() => { revalidateMultipleTags(['random-number']) router.refresh() }} > revalidate random-number </Button> </section> ) }
我們測試四種不同 revalidation 操作,分別針對:
特定 path 頁面 (Next.js 預設)
特定 path 但包含 layout 層級
子路徑
tag 為主的 cache
測試方式為判斷不同頁面的 random Number 是否有改變,藉此驗證網頁是否有成功刷新

驗證結果 : 不同 revalidate 操作的行為比較
操作方式 | 效果 | 備註 |
---|---|---|
revalidateMultiplePaths(['/'], 'page') | ✅ 會重新渲染 / 頁面 | 僅限於頁面型 component(如 page.tsx ) |
revalidateMultiplePaths(['/'], 'layout') | ✅ 所有 layout component 及底下所有 page 都會重新生成 | layout.tsx 底下所有子路由 |
revalidateMultiplePaths(['/qq']) | ✅ /qq 頁面會重新生成 | 同樣屬於 full static 頁面可觸發 revalidate,首頁不刷新 |
revalidateMultipleTags(['random-number']) | ⛔ 頁面不會重新生成,但資料會被更新 | 因為 force-static 是 full route cache,頁面 HTML 並不會重新建立 |
結語與建議
在使用 dynamic = 'force-static'
的情境下:
如果你只需要更新資料,不需要更新頁面 HTML,使用
revalidateTag
即可若你需要整頁 HTML 更新,務必使用
revalidatePath
type = layout
能協助你更新 layout 層級,對於全站性設計改動很有幫助千萬別以為 tag 清掉 cache 後頁面就會更新 —— 不會,除非頁面是 dynamic