⏱️

SSGとしてのAstroとNext.jsを比較してみた

2022/12/26に公開

Webサイトを構築するアーキテクチャとしてJamstackを導入する際、Static Site Generator (SSG) としての選択肢は豊富にあります。

HugoやGatsby、Next.jsあたりが定番だと思いますが、2022年8月にv1.0がリリースされたAstroが個人的に気になったので、Next.jsとビルドのパフォーマンスを比較してみることにしました。

中〜大規模サイトでの利用を想定して、API経由でコンテンツ取得する1000ページ分のサイトをNext.js、Astroそれぞれで生成して比べてみます。

なお、Next.jsはStatic Generationを利用します。

先に結論

  • 生成されたページのパフォーマンスはAstroのほうが有利
  • ビルド時間はNext.jsが早い。ただしやり方を変えればAstroのほうが早くなった。

準備編

Headless CMS(の代わり)

1000ページ分のデータを用意するのは手間がかかります。やりたいのはAPI経由でコンテンツを取得することなので、今回はランダムな文字列を返す簡易的なAPIをHeadless CMSの代わりとして用意しました。

ローカルにサーバーを立ててもいいのですが、せっかくなのでHonoを使って次のようなAPIサーバーを書き、Cloudflare Workersに公開してみました。WranglerというCLIがとても便利で、簡単に公開できました。

import { Hono } from 'hono';
const app = new Hono();

app.get('/:id', (c) => {
  const id = c.req.param('id');
  // ランダムな文字列を生成
  const body = Math.random().toString(32).substring(2);
  return c.json({ id, body });
});

export default app;

これで https://dummy-data-api.xxxxx.workers.dev/1 にアクセスすると次のようなレスポンスが返ってくるようになりました。

{"id":"1","body":"q142g79884"}

Next.js

pages/posts/[id].tsxを作成します。getStaticPathsで1000ページ分のパスを用意し、getStaticPropsでAPIを呼び出してデータを取得するようにしました。

pages/posts/[id].tsx
import { GetStaticPaths, GetStaticProps } from "next";
import Head from 'next/head';

interface Props {
  id: number;
  body: string;
}

export default function Page({ id, body }: Props) {
  return (
    <>
      <Head>
        <title>{body}</title>
      </Head>
      <small>Next.js version</small>
      <h1>id: {id}</h1>
      <h2>body: {body}</h2>
    </>
  )
}

export const getStaticPaths: GetStaticPaths = async () => {
  // 1000ページ分のpathを用意
  const paths = [...Array(1000).keys()].map((i) => {
    return {
      params: { id: (i + 1).toString() }
    }
  });
  return { paths, fallback: false }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  // API経由でコンテンツ取得
  const response = await fetch(`https://dummy-data-api.xxxxx.workers.dev/${params?.id}`);
  const { id, body } = await response.json();
  
  return { props: { id: data?.id, body: data?.body } }
}

Astro

src/pages/posts/[id].astroを作成します。コード自体はNext.jsとよく似ています。特にgetStaticPathsは名前もNext.jsと同じですし、中身もほぼ同じように書けました。

src/pages/posts/[id].astro
---
import Layout from "../../layouts/Layout.astro";

export function getStaticPaths() {
  // 1000ページ分のpathを用意
  return [[...Array(1000).keys()].map((i) => {
    return {
      params: { id: i + 1 }
    }
  })]
}

// API経由でコンテンツ取得
const { id } = Astro.params;
const response = await fetch(`https://dummy-data-api.xxxxx.workers.dev/${id}`);
const { body } = await response.json();
---

<Layout title={body || 'Not found'}>
  <small>Astro version</small>
  <h1>id: {id}</h1>
  <h2>body: {body || 'Not found'}</h2>
</Layout>

できあがったページ

ビルドして http://localhost:3000/posts/1 にアクセスするとそれぞれ次のようなページが1000ページ分できあがります。

Next.js Astro

比較してみる

準備が整ったので、さっそく比較してみます。今回は「生成されたページのパフォーマンス」と「ビルド時間」の2点を比べてみました。

ページのパフォーマンス

Chrome DevToolsのネットワークパネルを見てみます。

Next.js Astro

Astroはロードするリソースの少なさが際立ちます。Next.jsはHTMLをビルド時に生成するとはいえ、クライアントでhydrationを行うために、ミニマムなページであってもこれだけのリソースが必要になります。

DevToolsに新しく追加されたPerformance Insightsパネルも見てみます。

Next.js Astro

Largest Contentful Paint (LCP)の0.13s0.04sという数字の差はかなり驚きです。

ページのパフォーマンスという観点ではAstroが有利と言えそうです。

(なお、Next.jsが劣っているかというと決してそんなことはなく、私が働くUbieでもNext.jsを使った静的ページはパフォーマンス面で大きな問題を抱えていません。あくまで、Astroのほうがパフォーマンスを出しやすそう、ということです)

ビルド時間

では、ビルド時間はどうでしょうか。hyperfineというベンチマークツールを使って、ビルド処理にかかる時間を計測してみました。

Next.js

❯ hyperfine 'npm run build'
Benchmark 1: npm run build
  Time (mean ± σ):      9.675 s ±  0.483 s    [User: 17.264 s, System: 2.603 s]
  Range (min … max):    9.048 s … 10.555 s    10 runs

Astro

❯ hyperfine 'npm run build'
Benchmark 1: npm run build
  Time (mean ± σ):     22.342 s ±  0.535 s    [User: 7.807 s, System: 1.424 s]
  Range (min … max):   21.574 s … 23.284 s    10 runs

こちらはNext.jsの勝ち。Astroは2倍以上の時間がかかっています。通常、ページ数が増えれば増えるほどビルド時間はかかっていくので、大規模サイトでAstroを採用するのは少しためらってしまいます。

ビルド時間 再勝負

Astroだめかぁと思っていたところ、AstroでSSGする場合の個人的ベストプラクティス - console.lealog();という記事を見つけました。これによると、SSGをする場合は、ビルド時に毎回APIを呼ばず、先にデータを一括でAPI取得しておいて、ビルド時にはそのデータを参照すると効率がよいとあります。

そこで、APIの代わりに手元のJSONファイルからコンテンツを取得するように変更してみました。結果はこんな感じ。

Next.js

❯ hyperfine 'npm run build'
Benchmark 1: npm run build
  Time (mean ± σ):      8.884 s ±  1.386 s    [User: 17.152 s, System: 2.717 s]
  Range (min … max):    7.423 s … 11.599 s    10 runs

Astro

❯ hyperfine 'npm run build'
Benchmark 1: npm run build
  Time (mean ± σ):      3.672 s ±  0.227 s    [User: 4.295 s, System: 0.830 s]
  Range (min … max):    3.177 s …  3.918 s    10 runs

どちらもビルド時間は短縮されましたが、Astroは劇的に早くなり、Next.jsの半分以下になりました。(なぜここまで早くなるのか疑問。Next.jsのデータフェッチの効率が良すぎるという話かも?)

まとめ

生成されたページのパフォーマンスを見ると、Astroがかなり魅力的に映ります。ユーザー認証など複雑な処理が不要な、コンテンツ主体のWebサイトであれば、Next.jsよりもAstroのほうがメリットは大きいかもしれません。機会があればぜひ実務で試してみたいと思いました。

Discussion