🚅

Astro + MicroCMSで開発した個人ブログのビルドを高速化するtips

2024/01/24に公開

先日、私の個人ブログをNext.jsからAstroに移行しました。
https://ebifran-roadbike-blog.com/

無事に移行はできたものの、思ったよりもビルドに時間がかかるなという印象でした。以下は移行当初のCloudflare Pagesのビルドログです。

私の個人ブログは全部で50ほどのコンテンツしかないにも関わらず、ビルド開始から完了までに1分近く時間がかかっています。記事ページ1つをビルドするのに約1秒かかっており、記事数の増加に伴いビルド時間も増えていくことが目に見えていたため対応が必要でした。

原因を調査して改良を進めた結果、約1分かかっていたビルドが5秒前後で完了するようになりました。以下は改良後のビルドログです。

ここではAstroのビルドを高速化するために実施した改修内容について紹介します。

GetStaticPathsの戻り値propsを利用してデータ取得用apiの実行回数を減らす

改良前の記事詳細ページのコードは以下のようになっていました。(イメージ)
動的ルーティングのためにgetStaticPathsで登録済みの全記事のidを返しています。

[id].astro
---
export async function getStaticPaths() {
  // getAllArticleIds は全記事のidをapiで取得する関数
  const ids = await getAllArticleIds()
  return ids.map((id) => ({
    params: {
      id
    }
  }));
}

const { id } = Astro.params;

// getArticle はidをもとに記事の詳細情報をapiで取得する関数
const article = await getArticle(id);
---

// 詳細ページの描画...

上記のコードは、ビルド時に以下のように処理されます。

  1. getStaticPathsgetAllArticleIdsが1回実行される
  2. 記事ページの生成時、生成対象のページの数だけgetArticleが実行される

「2」より、記事の増加に伴ってapiの実行回数が増えてしまい、ビルド時間が遅くなるという問題があります。

これは以下のようなコードに修正することで解消しました。

[id].astro
---
export async function getStaticPaths() {
  // getAllArticles は登録済みの全記事の詳細情報をapiで取得する関数
  const articles = await getAllArticles()
  return articles.map((article) => ({
    params: {
      article.id,
    },
    props: {
      // 取得した全記事情報から、idをもとに対象の記事情報を特定してpropsに設定
      article: allArticles.find((x) => article.id === x.id) as Article
    },
  }));

}

// props経由で記事情報を取得
const { article } = Astro.props;
---

// 詳細ページの描画...

GetStaticPathsは、動的ルーティングに必要なparamsの他に、生成対象のページに渡す情報をpropsで返すことができます。(Next.jsとは異なる点ですね)

改良後のコードはビルド時に以下の様に処理されます。1度のapi実行で済むため、高速に動作します。

  1. getStaticPathsgetAllArticlesが1回実行される
  2. 記事ページの生成時、getStaticPathsの戻り値であるpropsに設定された記事情報をもとにページを生成する

私のように記事の管理にMicroCMSをお使いであれば、全ての記事情報を取得する処理はmicrocms-js-sdkのgetAllContents(v2.7.0から追加)を使うことで実現可能です。

apiの実行結果をキャッシュする

私の個人ブログは、ほぼ全てのページにサイドバーが存在します。サイドバーにはカテゴリとタグが表示されていますが、これはMicroCMS側の情報をapiで取得することで実現しています。

改良前は以下のような実装になっていました。(イメージ)

layout.astro
---
import SideBar from "@/components/sidebar.astro";
---

<div>
  <main>
    <slot />
  </main>

  <aside>
    <SideBar />
  </aside>
</div>
SideBar.astro
---
import { GetCategories, GetTags } from "@/libs/MicroCMS/Client";

// すべてのタグ情報とカテゴリ情報をapiで取得
const [tags, categories] = await Promise.all([GetTags(), GetCategories()])
---

// サイドバーの描画
libs/MicroCMS/Client.ts
// microcms-js-sdkを使ったデータ取得...
export const GetCategories = () => {
  return client.getAllContents<Category>(...)
};

export const GetTags = () => {
  return client.getAllContents<Tag>(...)
};

上記のGetTagsおよびGetCategoriesはタグとカテゴリの情報をapiで取得する関数です。ほぼすべてのページにサイドバーが存在することから、これらの関数はビルド時にページを生成するたびに実行されます。

サイドバーはすべてのページで全く同じ内容であるにもかかわらず、ページを生成するたびにapiを実行してしまっているという状態のため対応が必要です。

対応にあたり、まずapiの実行結果をキャッシュするための以下のような処理を用意しました。

libs/cache.ts
let cacheData: { [key: string]: any } = {};

const cache = async <T>(key: string, factory: () => Promise<T>) => {
  // キャッシュにない場合はデータ取得のための関数を実行して結果をキャッシュ
  if (!cacheData[key]) {
    cacheData[key] = await factory();
  }
  return cacheData[key] as T;
};

export default cache;

そのうえで、MicroCMS側のデータを取得する関数を以下の様に修正しました。

libs/MicroCMS/Client.ts
+ import cache from "@/libs/cache";

export const GetCategories = () => {
+  return cache("allCategoryList", () =>
    client.getAllContents<Category>(...),
+  );
};

export const GetTags = () => {
+  return cache("allTagList", () =>
    client.getAllContents<Tag>(...),
+  );
};

こうすることで、初回のみapiを実行して結果をキャッシュし、2回目以降はキャッシュを参照してデータを返すため高速に動作するようになります。

まとめ

私の場合、上記2つがAstroのビルド時間の短縮に非常に有効でした。移行した当初は思ったよりビルド時間が遅かったためAstroのせいにしそうでしたが、そんなことは全くなく自分のコードが悪いだけでした(泣)

この記事が参考になれば嬉しいです。

Discussion