【Next.js】SSG を利用したサイトアーキテクチャ


概要

本記事は、Next.jsのSSGを利用した技術とサイトの設計指針についてまとめました。

※ 私自身、Next.jsについてはまだ勉強中で、その備忘録として書いています。

SSG(Static Site Generation|静的サイト生成)

Next.jsでは、SSG(Static Site Generation|静的サイト生成)という技術が標準サポートされていて、それを使用することが推奨されています。SSGを使用すると、SEOに有利で、高速に表示されるサイトを作ることができます。

また、SSGを利用してAPIなど外部との通信が発生する処理(時間のかかる処理)を、サイトのビルド時に行い(Pre-fetch)、そのデータを埋め込んだHTMLをあらかじめサーバー側で生成することができます。

詳しく知りたい方は、公式ドキュメントを参照してください。英語ですが図説されているのでわかりやすいです。

Pre-rendering and Data Fetching

日本語がいい!という方は、以下の動画シリーズがおすすめです。

Deploy Hooks

Next.jsを開発しているVercel社は、Hostingサービスも提供しています。
このサービスでは、GitHubに連携してリポジトリ(mainなど)の更新に応じてリビルドする機能や、API経由でリビルドするDeploy Hooksが提供されています。

ISR(Incremental Static Regeneration|段階的な静的サイト生成)

ユーザーがサイトに訪れたときにサイトをリビルドして、次に訪れたユーザーに最新のサイトを提供する機能です。

ユーザーAが、Vercelサーバーにリクエストを送ります。
Vercelサーバーは、レスポンスとしてホスティングされたサイト(HTML)を返します。
③ ②と同時に、Vercelサーバーは、更新のあるページ(ISRを指定したページ)だけをリビルドします。
ユーザーBが、Vercelサーバーにリクエストを送ります。
Vercelサーバーは、レスポンスとして③で更新したサイト(HTML)を返します。

revalidateは更新頻度で、コード内でこれを指定することでISR機能を使うことができます。
上図では、ユーザーBは最新のサイトを閲覧できますが、ユーザーAは古いサイトを閲覧することになります。このことから、ISRは、閲覧回数が多いサイト向けの機能です。
revalidateを指定することで、高頻度のサイトアクセスによる過剰な更新を防ぎ、サーバー負荷を減らします。(ゲームでいうところのスキルのクールダウンみたいな感じです)

コード例

export const getStaticProps: GetStaticProps = async () => {
    const tasks = await getAllTasksData();
    return {
        props: { tasks },
        revalidate: 3
    };
};

getStaticPropsのreturnに、revalidateを指定することで、ISRに対応させたページにします。

Client side Fetching(CSF)

クライアントサイドで、効率よくAPIでデータを取得する方法として、SWRがあります。
キャッシュデータを使うことで過度なAPIリクエストを防ぎ、素早くサイトを表示させます。

これを使って何が嬉しいのかというと、ISRだけでは、ユーザーAは最新のサイトを閲覧できませんでした。
そこで、このSWRと組み合わせることによって、ユーザーAも最新のサイトを閲覧することができます。

下図は、SSG + ISR + CSF(SWR)を組み合わせた例です。


開発者がサイトをデプロイします。
②②´ Vercelサーバーは、SSGのPre-fetchによりビルド時にAPI経由でデータを取得します。
③③´ ユーザーAがサイトにアクセスします。
③″ クライアントサイドで、API経由で最新のデータを取得します(Client side Fetching)。
④④´ ③のリクエストをトリガーにして、ISRによってリビルドされます。このとき、②´同様にAPI経由でデータを取得します。
※③″と④´でAPIからデータを取得するタイミングはほぼ同じなので、同じでデータになるはずです。(秒単位で更新されるデータの場合は差異が生まれるかもしれません)
⑤⑤´ ユーザーBがサイトにアクセスします。
⑤″ ③″同様、クライアントサイドで、API経由で最新のデータを取得します(Client side Fetching)。
ユーザーAがサイトに訪れてから、ユーザーBが訪れるまでに、サーバーのデータが更新されていなければ、⑤´の内容と⑤″で取得する内容は同じになります。

コード例

const fetcher = (url: string) => fetch(url).then<TaskType[]>(res => res.json());

type PropsType = {
    tasks: TaskType[];
};

const TaskPage: VFC<PropsType> = ({ tasks }) => {
    const { data, error } = useSWR(`${process.env.NEXT_PUBLIC_RESTAPI_URL}/api/list-task/`, fetcher, {
        initialData: tasks, // ― 2
        revalidateOnMount: true // ― 3
    });

    // 以降、得られた data を使った処理
    ・・・
}

export default TaskPage

export const getStaticProps: GetStaticProps = async () => {
    const tasks = await getAllTasksData(); // ― 1
    return {
        props: { tasks },
        revalidate: 3 // ― 4
    };
};

① SSGのPre-fetchで、サーバーサイドでAPIデータを取得します。
useSWRのオプション、initialDataにサーバーサイド取得したAPIデータ(tasks)を渡します。
 これによって、クライアントサイドでデータが取得できるまでは、サーバーサイドで取得したデータを表示させます。
 ※このとき、APIデータに更新があると、古いデータ → 新しいデータとレンダリングされるので一瞬だけ古いデータが表示されます。
useSWRのオプション、revalidateOnMountを指定することで、コンポーネントがマウントされたときに必ずデータを取得するようします。
④ ISRに対応させることで、次にサイトを表示したときに、②で発生していた古いデータが一瞬だけ表示される現象が発生しないようにします。

サイト アーキテクチャ

これらの技術を使うことで、API経由で取得するデータの更新頻度APIの使用制限サイトのアクセス頻度に併せてサイトを設計することができます。

※ 以下のモデルは、あくまで一例です。

手動でリビルドする

API経由で取得するデータの更新に併せて、サイトを手動でリビルドします。

条件・状況
・API経由で取得するデータの更新頻度が低い場合(半年に1回、不定期など)
・APIの使用制限が厳しい場合(1時間あたりのリクエスト回数が決まっている、リクエスト回数に応じて料金が発生する)
・サイトのアクセス頻度が低い場合

Deploy Hooks を使う

Deploy Hookに、Cloud Functions(Cloud Functions for Firebase、 AWS Lambda など)を組み合わせることで、定期的に自動でリビルドするようにします。(毎日24:00にリビルドするHookを作る)

条件・状況
・API経由で取得するデータの更新頻度がそこそこの場合(数周間に1回、1日に1回など)
・APIの使用制限が厳しい場合(1時間あたりのリクエスト回数が決まっている、リクエスト回数に応じて料金が発生する)
・サイトのアクセス頻度が低い場合

ISR を使う

ISRの項目で紹介した設計を使います。

条件・状況
・API経由で取得するデータの更新頻度が高い場合(数時間に1回、数分に1回など)
・APIの使用制限がそこそこ厳しい場合(1時間あたりのリクエスト回数が決まっている、リクエスト回数に応じて料金が発生する)
・サイトのアクセス頻度が高い場合

ISR + Client side Fetching を使う

Client side Fetchingの項目で紹介した設計を使います。

条件・状況
・API経由で取得するデータの更新頻度が高い場合(数時間に1回、数分に1回など)
・APIの使用制限がゆるい場合(自社製のAPIサーバーを使うなど)
・サイトのアクセス頻度がそこそこ高い場合

まとめ

Next.js 面白い。

参考

この記事は、以下のコースの受講に伴って、備忘録としてまとめました。
もっと詳しく知りたい方は、是非受講してみてください。