Next.js の Incremental Static Regeneration を理解する

公開:2020/09/28
更新:2020/09/29
6 min読了の目安(約5900字TECH技術記事
Likes39

Next.js 9.4 から Incremental Static Regeneration という機能が実装されました。
段階的な静的サイト生成と訳されていて、SSG のビルドを最適化するものだろうなって認識しか無かったのですが自身のブログを ISR に対応させたので紹介していきます。

Next.js のビルドパターン紹介

Next.js のビルドにはいくつかパターンがあり、pages/ コンポーネントの処理に応じて適した出力をしてくれます。

どのパターンでビルドされてるかはログに書かれており、参考までにブログを yarn build した結果下記のように表示されました。

Page                                                           Size     First Load JS
┌ ● / (ISR: 1 Seconds)                                         AMP                AMP
├   /_app                                                      0 B            58.4 kB
├ ○ /404                                                       0 B            58.4 kB
├ ○ /about                                                     AMP                AMP
├ ● /article/[slug]                                            AMP                AMP
└ ● /articles/[tag]/[page]                                     AMP                AMP
+ First Load JS shared by all                                  58.4 kB
  ├ chunks/f6078781a05fe1bcb0902d23dbbb2662c8d200b3.5589eb.js  10.2 kB
  ├ chunks/framework.9ec1f7.js                                 39.9 kB
  ├ chunks/main.885dd3.js                                      7.28 kB
  ├ chunks/pages/_app.dd6249.js                                265 B
  └ chunks/webpack.e06743.js                                   751 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)

Server

Server Side Rendering (SSR)
getInitialProps もしくは getServerSideProps が使われている場合はこちらになります。
アクセス時にサーバーサイドで実行した結果をレスポンスします。アクセス毎に処理が走るので後述するパターンよりも Round-Trip Time が長くなりマシンリソースも消費します。
ユーザー認証後のページ等どうしても必要な時以外はほかのパターンを選択したほうがいいでしょう。

Static

サーバーサイドで実行する処理がなければこちらになります。ビルド時に生成した静的ファイルです。

SSG

Static Site Generation の略でビルド時に静的ファイルを生成します。
getStaticProps が使われている場合はこちらになります。
Static との違いはビルド時に処理を書くことでブログなら記事の数だけ記事ページが作れます。
Static と SSG はそのままホスティングサービスに設置できるのでサーバー側のリソースも要らず、CDN からレスポンスを返せるので Round-Trip Time が短くなり、サーバーサイドの処理を行わないためセキュリティ面でもメリットがあります。

ISR

Incremental Static Regeneration の略でアクセス時に静的ファイルを生成します。
getStaticProps が使われていて、revalidate が指定されてる場合はこちらになります。
今回取り上げるのがこの ISR です。

Incremental Static Regeneration とは何か

段階的な静的サイト生成のように訳されていて、SSG のように事前にすべてのページを生成するのではなく 1 度アクセスされた際にレスポンス内容が生成され、次回以降そちらの内容がレスポンスされます。

SSG のデメリットがいくつかあり、

  • 静的なページを生成する際にページ数が多いとビルドに時間がかかる
  • 1 度しかビルドしないので、再度すべてのページをビルドし直さないと内容が更新されない
    のような問題がありました。

ISR はそんな SSG の欠点を補うもので、次の動作で解決しています。

  • アクセス時に初めて生成されるので初回ビルドが高速
  • ISR でページ生成後も再度アクセスがあった際に次回以降の内容をビルドするので内容が更新される

ISR で pages コンポーネントを作る

通常

pages/index.tsx

import { NextPage } from 'next'

import { HomeTemplate } from '~/components/templates'

import { fetchArticles } from '~/lib/api'

export const config = { amp: true }

type Props = { articles: ArticleListItem[] }
const Home: NextPage<Props> = ({ articles }) => {
  return (
    <HomeTemplate articles={articles} />
  )
}

export const getStaticProps = async () => {
  const articles = await fetchArticles()

  return {
    props: { articles },
    revalidate: 1,
  }
}

export default Home

getStaticPropsrevalidate: 1 を返すと ISR になります。
revalidate の値は秒数で前回から何秒以内のアクセスを無視するか指定します。

Dynamic Routes の場合

pages/article/[slug].tsx

import { NextPage } from 'next'
import { useRouter } from 'next/router'
import ErrorPage from '~/pages/_error'

import { fetchArticle } from '~/lib/api'
import { ArticleTemplate } from '~/components/templates'

const Post: NextPage<{ article: Article }> = ({ article }) => {
  const router = useRouter()

  if (!router.isFallback && !article?.id) return <ErrorPage statusCode={404} />

  return (
    <ArticleTemplate>
      <div dangerouslySetInnerHTML={{ __html: article.body }} />
    </ArticleTemplate>
  )
}

export default Post

type StaticProps = { params: { slug: string } }
export const getStaticProps = async ({ params }: StaticProps) => {
  const article = await fetchArticle(params.slug)
  return {
    props: { article },
    revalidate: 1,
  }
}

export const getStaticPaths = async () => ({
  paths: [],
  fallback: true,
})

revalidate のほかに getStaticPathsfallback を指定する必要があります。
fallback はアクセスされた URL のファイルが存在しない場合の挙動を決めるもので、true の場合はファイルが存在しなくても 404 エラーを返しません。
このコードの場合だと、URL に含まれる slug を元に getStaticProps で記事を取得して pages コンポーネントに記事を渡します。このとき記事が存在しなかった場合、 pages コンポーネント内で 404 エラーページを表示させる必要があります。

Dynamic Routes (AMP対応) の場合

pages/article/[slug].tsx

import { NextPage } from 'next'
import { useRouter } from 'next/router'
import ErrorPage from '~/pages/_error'

import { fetchArticle } from '~/lib/api'
import { ArticleTemplate } from '~/components/templates'

export const config = { amp: true }

const Post: NextPage<{ article: Article }> = ({ article }) => {
  const router = useRouter()

  if (!router.isFallback && !article?.id) return <ErrorPage statusCode={404} />

  return (
    <ArticleTemplate>
      <div dangerouslySetInnerHTML={{ __html: article.body }} />
    </ArticleTemplate>
  )
}

export default Post

type StaticProps = { params: { slug: string } }
export const getStaticProps = async ({ params }: StaticProps) => {
  const article = await fetchArticle(params.slug)
  return {
    props: { article },
    revalidate: 1,
  }
}

export const getStaticPaths = async () => ({
  paths: [],
  fallback: 'unstable_blocking',
})

export const config = { amp: true } の場合、fallbackunstable_blocking を指定する必要があります。この指定がドキュメント上で見つからず、PR 上でしか見つかりませんでした。

デプロイ後の確認

ブログ で無事に ISR 対応ができました。
デプロイ後に記事ページをアクセスすると、初回は事前に用意されたレスポンスが存在しないので少し時間がかかり、次回以降はすぐレスポンスが返ってくるのが確認できました。
デプロイ環境には Vercel を使っていて、サーバーサイドでレスポンスする内容を生成するときに Functions が実行されていました。