Next.js 的 revalidate 跟 force-static 搭在一起到底會發生什麼事?

2025-05-202.52Front EndNextjsReactNext.js 的 revalidate 跟 force-static 搭在一起到底會發生什麼事?

最近在玩 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_cachecache 將隨機數據標籤為 random-number,代表資料本身是可被 tag revalidation 的。

注意 : 如果 unstable_cache revalidate 參數沒有帶,則資料永遠 cache,直到使用 revalidatePathrevalidateTag  可參考 官方文檔


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


參考

Functions: unstable_cache

Data Fetching and Caching

cache – React 中文文档