Next.jsはGatsby.jsを倒さない


最近Next.jsがめでたくv10がリリースされたこともあってNext.jsの名前を聞く機会は増えていると思います。
Next.jsの特徴で調べると「SSG+SSRが出来るフルスタックなフレームワーク」として出てきますが、そのことによってGatsby.jsは打倒されるのではないかという疑問をよく目にするようになっていると思います。
私個人の意見として、現状Next.jsがGatsby.jsを完全に置き換えられるかという問いに対してノーと言えます。
その理由は、各フレームワークがSSGを実現するその仕組みについてを知り強み弱みを理解することで納得できるものとなるでしょう。

各フレームワークによるSSGの実行プロセス

例えば、あなたがブログサービスを作りたいとして以下の要件を決めた場合、Next.jsとGatsby.jsでSSG面に関して実装の差を見比べましょう。

要件
- url形式は /blogs/[記事id] にしたい
- トップページに記事一覧を表現したい
- 別フレームワークにてapiを開発を行うものとして自由に作れるが、`https://.../blogs`で全てのブログ情報を取得する想定で進める

Gatsby.js

Gatsyby.jsには2種類の実装方法があります

  • pagesディレクトリにコンポーネントを作成する手順
  • gatsby-node.jsにcreatePagesを定義する手順

ここではgatsby-node.jsに記載する手順で進めます。

gatsby-node.js
const path = require(`path`)

exports.createPages = async ({ actions }) => {
  const { createPage } = actions
  const blogTemplate = path.resolve(`src/templates/blog.js`)
  const topTemplate = path.resolve(`src/templates/top.js`)
  
  const res = await fetch('https://.../blogs')
  const blogs = await res.json()
  blogs.forEach(blog => {
    createPage({
      path: `/blogs/${blog.id}`,
      component: blogTemplate,
      context: {
	blog,
      },
    })
  })
  const links = blogs.map(blog => ({ url: `/blogs/${blog.id}`, title: blog.title })
  createPage({
    path: '/',
    component: topTemplate,
    context: {
      links,
    },
  })
}

Next.js

SSGをしたい場合各ページコンポーネント上でgetStaticPaths()とgetStaticProps()を定義して、作っていきます。

/pages/blogs/[id].jsx
function Blog({ blog }) {
  ...
}

export async function getStaticPaths() {
  const res = await fetch('https://.../blogs')
  const blogs = await res.json()
  const paths = blogs.map((blog) => `/blogs/${blog.id}`)
  return {
    paths,
    fallback: true,
  }
}

export async function getStaticProps({ params }) {
  const res = await fetch('https://.../blogs')
  const blogs = await res.json()
  const blog = blogs.find(blog => params.id === blog.id)

  return {
    props: {
      blog,
    },
  }
}

export default Blog
/pages/index.jsx
function Top({ links }) {
  ...
}

export async function getStaticProps() {
  const res = await fetch('https://.../blogs')
  const blogs = await res.json()
  const links = blogs.map(blog => ({ url: `/blogs/${blog.id}`, title: blog.title })

  return {
    props: {
      links,
    },
  }
}

export default Top

Diff

一見するとGatsby.jsは巨大関数になっており、Next.jsは目的ごとに関数分けされていることから優れているように見えますが、重要な違いがあります。
各ページ毎にgetStaticPathsとgetStaticPropsを実行してページを生成します。
仮にblogのモデルが3レコードある場合、

  • pages/blogs/[id].jsx
    • getStaticPaths // 1回
    • getStaticProps // 3回
  • pages/index.jsx
    • getStaticProps // 1回

合計5回リクエストされることになります。ブログのページではいわゆるN+1状態になっていることがよく分かります。
一方Gatsby.jsではページ生成処理全体で1メソッドとなっているためapiのリクエストは1度で済みます。

Next.jsが提供するISR

上記のような多数リクエストに対しての解決策として、Next.jsではISR(Incremental Static Regeneration)という仕組みを提供しました。
これはビルド時に生成するのではなくて、アクセス時にそのページを生成するという仕組みです。
pages/blogs/[id].jsxは以下のように書き換えると出来ます。

pages/blogs/[id].jsx
function Blog({ blog }) {
  ...
}

  export async function getStaticPaths() {
-   const res = await fetch('https://.../blogs')
-   const blogs = await res.json()
-   const paths = blogs.map((blog) => `/blogs/${blog.id}`)
   return {
-     paths,
+     paths: [],
      fallback: true,
   }
 }

export async function getStaticProps({ params }) {
  const res = await fetch('https://.../blogs')
  const blogs = await res.json()
  const blog = blogs.find(blog => params.id === blog.id)

  return {
    props: {
      blog,
    },
+   revalidate: 15 * 60,
  }
}

export default Blog

revalidateの単位は秒です。上記の場合15分の設定となっています。

以上のように変更を行うことで、ビルド時のリクエスト回数は0になります。
代わりにrevalidateに指定した時間を超えて該当ページにアクセスしてきた場合、キャッシュを行うためにリクエストがされます。
これにより、1記事を変更したことによって全てのファイルのビルドをし直す必要も無くなりかつビルドを行うための大量の時間とリソースを必要としなくなるというメリットがあります。
一方でまだ細かいキャッシュ設定は出来ないためにビルド後も定期的に低負荷なリクエストがされ続けるというデメリットがあります。

revalidateの単位を無期限に近い数値にすることでリクエストの数を減らせるのでは?と思いまれますが、それを行ってしまうとデータに誤りがあった場合に修正を行っても内容が反映されないという恐れがあるので、個人的にはオススメできません。

Gatsby.js vs Next.js

以上を用いてこの二つの強み、弱みを比較しましょう。

Gatsby.js

  • 強み
    • ビルド時のページ生成時はリソースを意識してきめ細かくページ生成を行える
    • 歴史が長いため、豊富なプラグインやテーマのサポートが受けられる
    • 完全性が求められるサイトに向いている
  • 弱み
    • ページ数に応じてビルド時間は肥大化していく
    • 静的サイト目的のみでしか利用できない

Next.js

  • 強み
    • ISRを利用することでビルド時間、負荷の分散が行える
    • 特定のページのビルドに失敗してもシステムを他のページは開け、システムを落とすことはしなくて済む
    • 完全が求められない静的サイトには向いている
  • 弱み
    • 通常のSSGを行おうとするとN+1リクエストになってしまう

ここで書かれている "完全性" とは、404などの画面を出しづらい。という意味で利用しています。
ISRの場合、記事データを削除してもキャッシュの方で記事一覧のページのみ一時的に残ってしまうという事例が起こりえます。

まとめ

ISRはまだ実験的機能ではありますが、Next.jsが考える未来というのはこれを強めていくと期待しています。
そうした場合にISRのNext.js、SSGのGatsby.jsとして比較することになり両者が共存する世界が訪れると考えています。