🧚

ローカルキャッシュを使って Next.js SSG 実装を少し幸せにする

2022/11/09に公開

Next.jsを利用して、外部APIからのデータにもとづく静的サイトを作っていると、毎回同じAPIルート(同じデータを返す)への呼び出しに頭を抱えることが多いです。

export const getStaticPaths: GetStaticPaths = async () => {
  const articles = await getArticles(); // API呼び出し
  
  const paths = [ ... ]
  
  return {
    paths: paths,
    fallback: true,
  };
};

Vercelにデプロイすれば、ビルド時に呼ばれるだけですが、ローカルだとリクエストごとに処理されるため、開発効率を低下させます。それも、ごく少数のデータなら問題にならないですが、通常は静的サイトをつくるのに事前レンダリングしたいパスを指定するため、結構な件数を取得する場合が多いと思います。

また、開発時はローカルのDBや、低スペックのサーバ構成で実装を続けるので、それもストレスの原因だったりします。

ということで、SSG 実装時においてAPI通信をあまり発生させない対策を考えました!

手始めに

まず考えたのは、開発環境とVercel環境で取得するクエリを変えるです。簡単に言えば、 getStaticPaths ではデータ取得を行わず、レンダリング時にクライアント側で取得する方法です。

export const getStaticPaths: GetStaticPaths = async () => {
  // 開発時は空データを返してAPI通信しない
  const articles = process.env.IS_LOCAL ? [] : await getArticles();
  
  const paths = [ ... ]
  
  return {
    paths: paths,
    fallback: true,
  };
};

これは、開発時には大きな問題もなく、しばらく運用していました。が、APIの取得処理をmockする形になるため、サーバーサイドと同時並行で開発を続けていると、Vercelへのデプロイ時に、はじめてバグが見つかることが頻発したため、早々に諦めました。

APIレスポンスをローカルでキャッシュする

その後も遅いレンダリングにうなされながら過ごした末、たどり着いた解決策は、APIレスポンスをローカルに .jsonファイル としてキャッシュすることでした。

まずはじめに、.jsonファイル への読み取り&書き込みするヘルパーメソッドを生やしておきます。

fsHelper.tx
import fs from 'fs';
import { promisify } from 'util';

export const readFile = promisify(fs.readFile);
export const writeFile = promisify(fs.writeFile);

これをAPIのリクエスト処理前に、キャッシュがあれば取り出す形にします。このとき、CAN_USE_CACHE といった環境変数を用意して、Vercelの場合は処理自体をスキップさせます。

index.tsx
import path from 'path';

export const getArticless = async () => {
  // キャッシュがあれば取り出す
  if (process.env.CAN_USE_CACHE) {
    const articles = cachedArticles();
    if (articles) return articles();
  }

  // API
  const articles = await fetchArticles();
  if (process.env.CAN_USE_CACHE) {
    // キャッシュ化
    writeFile(cache, JSON.stringify(articles), 'utf8').catch(() => {});
  }

  return articles
}

export const getStaticProps = async ({ params: { id } }) => {
  const article = process.env.CAN_USE_CACHE
    ? cachedArticles().filter((article) => article.id == id)[0] // キャッシュ
    : await fetchArticle('id'); // API

  return {
    props: {
      article: article,
    },
    revalidate: 60,
  };
}

const cachedArticles = async () => {
  const cache = path.resolve('.articles_index_data')
  return JSON.parse(await readFile(cache, 'utf8')) as Article[];
}

こうすることで、APIをmock化することなく、2回目以降はキャッシュを使った高速なレンダリングが実現できました。なお、キャッシュを消したいときは、ローカルに保存されたデータを物理削除すればOKです👌

ベンチマーク

1,000件の記事データを処理したベンチマークです。
1回目は、ローカルDBにAPIリクエストして、2回目はキャッシュデータを使うことで、その差は歴然でした。

ま、通信がないので早いのは当たり前ですが・・・😅

1回目(APIリクエスト時)

2回目(キャッシュ時)

ということで、ローカルキャッシュを使って Next.jsのSSG実装を少し幸せにするTipsでした。

では

Collections

Discussion