Incremental Static Regeneration

Examples
Version History
VersionChanges
v12.2.0On-Demand ISR is stable
v12.1.0On-Demand ISR added (beta).
v12.0.0Bot-aware ISR fallback added.
v9.5.0Base Path added.

Next.js 允許你在建置完畢以後添加新的頁面或更新。「增量靜態生成(Incremental Static Regeneration,ISR)」讓你不需要重新建置整個網頁就能基於原先的頁面做靜態生成,使用 ISR 能夠讓你在擴大頁面規模時依然享有靜態生成的好處。

開啟 ISR 的方式是在 getStaticProps 中新增 revalidate 這個 props:

function Blog({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } // getStaticProps 一般只會在伺服端的建置期間被執行 // 不過只要設置 revalidate 並接收到新的請求,就可以讓它在不需要伺服器的情況下被重新執行。 export async function getStaticProps() { const res = await fetch('https://.../posts') const posts = await res.json() return { props: { posts, }, // Next.js 會試著重新生成該頁面,如果: // - 接收到請求 // - 距離上一次的請求時間超過 10(秒) revalidate: 10, } } // getStaticPaths 一般只會在伺服端的建置期間被執行 // 但如果碰到尚未生成的路徑時,他可能會在沒有伺服器的情況下被重新執行 export async function getStaticPaths() { const res = await fetch('https://.../posts') const posts = await res.json() // 根據預先渲染的 posts 來產生相對應的路徑 const paths = posts.map((post) => ({ params: { id: post.id }, })) // paths 只會在建置時間做預渲染時被生成。 // 但如果設置 { fallback: 'blocking' } 且請求的路徑不存在時將會在伺服端重新生成 return { paths, fallback: 'blocking' } } export default Blog

當對一個已經被預先渲染的頁面發出請求時,第一次會先顯示快取頁面。

  • 接下來的請求若距離上次不超過 10 秒的話一樣會顯示快取頁面
  • 10 秒過後的下一個請求依然會顯示快取(已過期)頁面
  • 此時會觸發 Next.js 在背景重新生成頁面
  • 如果頁面成功生成,Next.js 將會廢除原先的快取並顯示更新後的頁面,否則將保留原始頁面

當請求一個尚未生成的路徑時,第一次 Next.js 會在伺服端重新生成新的頁面。往後再碰到相同的請求時將會從快取中傳回靜態檔案。詳細可閱讀 ISR 在 Vercel 上的 全域保留快取及處理回滾(rollbacks)

注意:請確認你的上游資料供應者是否預設啟用快取機制,若是有你可能需要其停用(例如 useCdn: false),否則重新驗證將無法抓取新的內容並更新到 ISR 快取。快取亦有可能出現在帶有 Cache-Control 標頭的 CDN(被請求的端口) 上。

按需(On-demand)重新驗證

如果你把 revalidate 的時間設為 60,則所有在一分鐘以前的造訪者都會看到同一版的頁面,直到一分鐘以後的某個人造訪時才會清除快取頁面。

自從 v12.2.0 發佈後 Next.js 支援對指定頁面來依照需求做靜態生成(ISR)以便手動清除快取,並讓你能夠在底下幾個情境中更方便的對網站做更新:

  • 在 Headless CMS 上建立或更新了內容
  • 在電子商務平台上更新了資訊(價格、敘述、種類、預覽,等等...)

要使用按需重新驗證的功能,你不需要在 getStaticProps 中設置 revalidate。如果 revalidate 被省略的話 Next.js 會套用預設值 false(也就是不做重新驗證)並等到 revalidate() 被呼叫時時才會做重新驗證。

注意: 中間件 並不會在收到按需的 ISR 請求時被執行。因此你應該對妳想重新驗證的 對應 頁面呼叫 revalidate()。舉例來說,如果你有一個 pages/blog/[slug].js 的頁面並且把 /post-1 覆寫到 /blog/post-1,那麼你應該用 res.revalidate('/blog/post-1') 的方式來呼叫。

使用按需重新驗證

首先為你的 Next.js 應用建立只有你知道的秘密令牌(token),這是要用來避免未經授權的人存取重新驗證的 API 路徑。你可以透過底下的 URL 結構來存取該路徑(可以手動配置或搭配 webhook):

https://<your-site.com>/api/revalidate?secret=<token>

接著將令牌以 環境變數 的形式傳入到你的應用中。最後,建立重新驗證的 API 路徑:

// pages/api/revalidate.js export default async function handler(req, res) { // 透過令牌來檢查該請求是否合法 if (req.query.secret !== process.env.MY_SECRET_TOKEN) { return res.status(401).json({ message: 'Invalid token' }) } try { // 這裡應該要是完整的路徑 // 例如:"/blog/[slug]" 會對應到 "/blog/post-1"(而不是 "/post-1") await res.revalidate('/path-to-revalidate') return res.json({ revalidated: true }) } catch (err) { // 如果碰到錯誤的話 Next.js 會繼續顯示上次成功生成的頁面 return res.status(500).send('Error revalidating') } }

看我們的示範 來瞭解按需重新驗證的實際運作並提供反饋。

在開發期間測試按需 ISR

當你在本地運行 next devgetStaticProps 會在每一次請求被觸發。如果要驗證你的按需 ISR 設置是否正確,你需要建立 生產環境建置(production build) 並開啟 生產環境伺服器(production server)

$ next build $ next start

接著你就可以檢查該靜態頁面是否有正確的被重新驗證。

錯誤處理與重新驗證

getStaticProps 在背景運行重新生成時出現錯誤(或者是你手動拋出的錯誤),會繼續顯示上次成功生成的頁面。接著再碰到下個請求時 Next.js 會再執行一次 getStaticProps

export async function getStaticProps() { // 如果這個請求出現錯誤,Next.js 將廢除原本要顯示的頁面, // 並在下一個請求重新執行 getStaticProps。 const res = await fetch('https://.../posts') const posts = await res.json() if (!res.ok) { // 如果碰到伺服器發生錯誤,你可能會想拋出錯誤來避免更新快取, // 直到下次請求成功。 throw new Error(`Failed to fetch posts, received status ${res.status}`) } // 如果請求成功的話就回傳文章列表, // 以及每 10 秒重新驗證一次 return { props: { posts, }, revalidate: 10, } }

自主託管(Self-hosting)ISR

ISR 適用於 自主託管 Next.js 網站,只要運行 next start 就可以開箱即用。

你可以透過這種方式來部署到像是 KubernetesHashiCorp Nomad 這一類容器協調器(container orchestrators)。在預設情況,生成的資源會被儲存在每一個容器(pod)的記憶體中,代表每一個容器會有自己的靜態檔案副本,所以在指定的容器被請求以前有可能會顯示舊的資料。

為了確保所有容器的一致性,你可以停用記憶體快取。這樣子會讓 Next.js 伺服器只使用 ISR 在檔案系統中生成的資料。

你可以在你的 Kubernetes 容器(或其他類似的配置)中使用網路共享掛載,讓不同的容器(containers)間能夠重用相同的檔案系統快取。在使用相同掛載時 .next 資料夾(包含 next/image 的快取資料)也能被共享及重用。

要停用記憶體快取,可以在你的 next.config.js 中把 isrMemoryCacheSize 設為 0

module.exports = { experimental: { // 預設值為 50MB isrMemoryCacheSize: 0, }, }

注意:你必須考慮在不同容器間同時更新快取時的 競爭條件(Race condition),取決於你怎麼為共享掛載做設置。

相關

想了解更多下一步該做什麼的資訊,我們建議閱讀下面幾個章節:

本篇文章由jubeatt

jubeatt

貢獻翻譯。瞭解如何參與貢獻