microCMS

microCMS + AstroのJamstack構成でキャッシュを活用し、ビルド時間を90%以上短縮した方法

岸本 彬

はじめまして、株式会社メンバーズ メンバーズルーツカンパニーの岸本です。

普段はフロントエンドエンジニアとして、主にmicroCMSとAstroを使用したJamstack構成の開発を行っています。

メンバーズルーツカンパニーでは、今までにmicroCMSとAstroを利用したJamstack環境での開発を複数行ってきました。

今回はその中で直面した、ビルド時間に関する課題とその解決方法として、キャッシュ処理を用いたビルド時間の短縮方法をご紹介いたします。

背景と課題

記事詳細ページのビルドに時間がかかることに気づく

Jamstack構成のウェブサイトを構築するにあたってネックになるのがビルド時間です。事前に全ページをビルドする必要があるため、ビルドには相応の時間がかかります。

弊社の自社サイトをWordPressの構成からmicroCMS + Astro構成に変更したのですが、開発中、記事詳細ページのビルドに異様に時間がかかることに気づきました。

Astroではビルド時に1つのページごとにどれくらいの時間がかかったのかをログで確認することができます。ビルドログを確認してみたところ、1つの記事のビルドに記事一覧ページ1つをビルドしているのと同じくらいの時間がかかっていました。

▼記事一覧ページのビルドログ
ビルドに778ミリ秒かかっています。

記事詳細ページのビルドログ
1ページで約780ミリ秒かかっています。

当サイトはサイト全体で220ページあるのですが、全ページのビルドが完了するのに251秒かかっていることが分かりました。

そこで、改めて記事詳細ページの構成と処理を確認したところ、記事詳細ページの一番下に他のおすすめ情報として、他の記事一覧を設置していました。(赤枠で囲んでいるエリア)

https://roots.members.co.jp/information/230726/

この箇所は毎回記事一覧を取得し、リストとして実装していました。
つまり、 詳細記事を1ページビルドするごとに毎回記事一覧も取得していたことになります。1つの記事のビルドに記事一覧ページ分のビルド時間がかかっていたのも理解できました。

解決へのアプローチ

記事詳細ページ下の他のおすすめ情報は、詳細記事の内容に合わせてカテゴリごとに出し分けていますが、データのリソースとしては毎回同じ記事一覧を取得しています。

そのため、一度取得すれば使いまわすことができ、ビルド時間が短縮できると考えました。

実装について

実装の流れ

以下のような実装を考えました。

  1. キャッシュ用の変数を用意する。
  2. キャッシュが空ならmicroCMSのAPIにアクセスしデータを取得、取得したデータはキャッシュ用の変数に保存する。
  3. キャッシュがあれば、APIにアクセスせずにそのままキャッシュしたデータを利用する。

キャッシュ処理を入れる前のビルド処理

キャッシュ処理を入れる前の記事詳細ページの処理はこのようになっています。
自社サイトの開発環境では、microcms.ts というファイルにmicroCMS関連の処理を記載しています。

// src/library/microcms.ts

// 全ての記事を取得
export const getAllContentList = async <T>(
  apiName: string,
  queries: MicroCMSQueries = {}
): Promise<Array<T & MicroCMSContentId & MicroCMSDate>> => {
  const contents = await client.getAllContents<T>({
    endpoint: apiName,
    queries
  })
  return contents
}

記事詳細ページのastroファイルはこのようになっています。

// src/pages/information/[information_id].astro

---
import { getAllContentList } from '@/library/microcms'
import type { ArticleType } from '@/library/microcms'

export async function getStaticPaths() {
  const response = await getAllContentList<ArticleType>('information', {fields: ["id"]})
  return response.map((content) => ({
    params: {
      information_id: content.id
    },
    props: { content }
  }))
}

// 全記事を取得する
const otherPosts = await getAllContentList<ArticleType>('information')
---
// 描画部分

microcms-js-sdkを使用して getAllContentList という関数を microcms.ts に作成し、記事詳細ページのAstro上で1ページのビルドごとにコンテンツを取得していました。

これでは、getStaticPathsawait getAllContentList<ArticleType>('information', {fields: ["id"]}) を実行し、特定の記事データを取得した後、さらに全記事を取得する await getAllContentList<ArticleType>('information') が実行されている状態です。

キャッシュ用の変数を用意

そこで、一度記事一覧を取得したら、取得したデータをキャッシュに保存するようにします。
microCMS関連の関数は microcms.ts に記載しているのでここにキャッシュ用の変数を作成しました。

// src/library/microcms.ts

// キャッシュ用の変数
const microCMSCache: {
  [K in string]: unknown
} = {}

このキャッシュ用変数の使用するイメージとしては、以下のようにエンドポイントごとにデータを保管できるような形です。

{
  "information": [おすすめ情報データの配列]
  "news": [ニュースデータの配列]
}

キャッシュ処理を作成

次にキャッシュ処理を作成します。

データ取得を開始した際にキャッシュ用の変数にデータが格納されていればそれを返し、格納されていなければAPIから取得するといった動作をするようにします。

// src/library/microcms.ts

const microCMSCache: {
  [K in string]: unknown
} = {}

export const getCachedContents = async <T>(apiName: string): Promise<Array<T & MicroCMSContentId & MicroCMSDate>> => {
  // キャッシュされていなければmicroCMSのSDKから取得
  if (microCMSCache[apiName] === undefined) {
    const contents = await getAllContentList<T>(apiName)
    console.log(`microCMS: キャッシュに登録[${apiName}]`)

    // キャッシュ用の変数に格納
    microCMSCache[apiName] = contents

  } else {
    console.log(`microCMS: キャッシュから取得[${apiName}]`)
  }
  return microCMSCache[apiName] as Array<T & MicroCMSContentId & MicroCMSDate>
}

ビルド時にキャッシュを利用しているかどうか分かりやすいように console.log でログが出るようにしました。

Astroの記事一覧取得処理を修正

キャッシュ処理をしてくれる関数の作成ができたので、astroファイルの記事一覧取得処理を修正します。
記事一覧を取得している箇所を全て getCachedContents に置き換えます。

// src/pages/information/[information_id].astro
---
import { getCachedContents } from '@/library/microcms'

export async function getStaticPaths() {
  // const response = await getAllContentList<ArticleType>('information', {fields: ["id"]})
  const response = await getCachedContents<ArticleType>('information')

  return response.map((content) => ({
    params: {
      information_id: content.id
    },
    props: { content }
  }))
}

const { content } = Astro.props
// const otherPosts = await getAllContentList<ArticleType>('information')
const otherPosts = await getCachedContents<ArticleType>('information')
---

// 描画部分

実際に動作しているかを確認する

実際にビルドしました。

記事詳細ページあたり約 8ミリ秒程度でビルドできてることがわかります。

もともと平均的に780ミリ秒かかっていたため、772ミリ秒の短縮に成功しました。
しっかり最初にキャッシュ登録がされていて、それ以降はキャッシュから取得していることがログからも分かります。

よく使うフィルタ、ソートなどの実装

キャッシュ処理はできたのですが、このキャッシュ機能の課題として、microCMSが用意してくれているフィルタやソートが使えなくなるという問題があります。

キャッシュしたものを使い回すため、フィルタ付きでキャッシュしてしまうと、他のところで使う場合もフィルタ付きになってしまいます。
そのため、フィルタやソートを使いたい場合は以下のように自前で実装する必要があります。

const otherPosts = (await getCachedContents<ArticleType>('information'))
  .sort((a, b) => {
    return new Date(a.publishedAt ?? a.createdAt).getTime() >
      new Date(b.publishedAt ?? b.createdAt).getTime()
      ? -1
      : 1
  })
  .slice(0, 10)

このフィルタは取得した記事を日付順に並べ替え、件数を10件に絞っています。

ただ、日付順や件数を絞るなどはいろんな場面で使うため、getCachedContents の引数で使用したいフィルタを受け取り、内部で処理することで楽にフィルタリングできるようにしました。

並べ替えと件数制限のフィルターを自作した例がこちらになります。

// src/library/microcms.ts
const microCMSCache: {
  [K in string]: unknown
} = {}

interface QueriesType {
  limit?: number
  order?: "publishedAt"
}

export const getCachedContents = async <T>(apiName: string, queries: QueriesType): Promise<Array<T & MicroCMSContentId & MicroCMSDate>> => {
  // キャッシュされていなければmicroCMSのSDKから取得
  if (microCMSCache[apiName] === undefined) {
    const contents = await getAllContentList<T>(apiName)
    console.log(`microCMS: キャッシュに登録[${apiName}]`)

    // キャッシュ用の変数に格納
    microCMSCache[apiName] = contents
  } else {
    console.log(`microCMS: キャッシュから取得[${apiName}]`)
  }

  let contents = microCMSCache[apiName] as Array<T & MicroCMSContentId & MicroCMSDate>
  // フィルタ処理の例
  // order
  if (queries.order !== undefined){
    contents = contents.sort((a, b) => {
      return new Date(a.publishedAt ?? a.createdAt).getTime() > new Date(b.publishedAt ?? b.createdAt).getTime() ? -1 : 1
      })
  }

  // limit
  if (queries.limit !== undefined){
    contents = contents.slice(0, queries.limit)
  }

  return contents
}


キャッシュ処理によるビルド時間短縮の効果

キャッシュ処理を導入し、以下のようなサイトでビルド時間が短縮できました。

1. 自社サイト

ページ数:220ページ
ビルド時間:251秒から14秒に短縮
ページ構成:記事詳細ページ下部に記事一覧がある、トップページに記事一覧が複数ある、カテゴリやタグごとの記事一覧がある

2. 金融系企業A社

ページ数:38ページ
ビルド時間:392秒から72秒に短縮
ページ構成:記事一覧が年度ごとに分けられている、サイドバーに記事年度一覧がある

サイトのイメージ
https://example.com/news/2024

3. 金融系企業B社

ページ数:221ページ
ビルド時間:121秒から46秒に短縮
ページ構成:トップページに複数の記事一覧がある、カテゴリやタグごとの記事一覧がある

サイトのイメージ
https://example.com/top

ビルド時間を短縮できるページ構成例

ビルド時間を短縮できるページ構成例として以下のようなページがあげられます。

詳細ページに記事一覧がある

今まで説明した通り、一度取得しキャッシュした記事一覧を使い回すことでビルド時間を短縮することができます。

サイドバーやフッターに記事一覧がある

サイドバーに記事一覧がある場合も同様に、記事一覧を使い回すことでビルド時間を短縮することができます。
サイドバーの場合、記事の詳細ページ以外にも設置されている場合が多いかと思うので、上記の詳細ページに記事一覧などがある場合よりも効果が発揮できると思います。

カテゴリやタグが複数ある

カテゴリやタグが複数あり、それぞれカテゴリごとの記事一覧ページなどがある場合、その分ビルドするページが増えると思います。
キャッシュを利用することで自前でフィルターが必要にはなりますが、ビルド時間を短縮することができます。

トップページに複数の記事一覧がある

トップページに複数の記事一覧を表示する場合も、キャッシュを利用することでトップページのビルド時間が短縮できます。

記事一覧が年度ごとに分かれている

N案件で対応した内容ですが、年度ごとに別れているため、年度の計算が必要になります。
年度の計算の際は複数の年数を取得する必要があるかと思いますが、キャッシュ機能があることで1度取得したものを使いまわして計算することができます。

今後の課題

今後の課題として以下のようなものが考えられます。

  • 記事ごとに個別のフィルターを使用している場合など(例えば、記事ページの「その他の記事」のところに自分の記事をのぞいた全記事表示など)、キャッシュが意味を成さない問題
  • microCMSのフィルターを利用できないため、フィルター処理を自作する必要がある問題


解決するのは難しい課題だとは思いますが、より良いキャッシュ処理を今後も考えていこうと思います。

おわりに

ビルド時間が早くなることでmicroCMSで公開、非公開を行った際の反映速度が早くなる他、ローカル環境でのビルドテストや、開発モードでの実行もキャッシュの恩恵を受けることができます。

みなさんもこれを参考にビルド時間短縮を試していただけたら幸いです。

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

岸本 彬
株式会社メンバーズ メンバーズルーツカンパニーにて主にフロントエンドエンジニアとして業務しています。最近はmicroCMSとAstroを使ったJamstack構成の開発を行っています。
株式会社メンバーズ 認定パートナー
運用で社会を変革するデジタルクリエイター集団です。弊社は、microCMSを活用して、国内金融企業さまへの『デジタル運用特化』でお客様のビジネス変革・成果向上の内製化を推進します。
https://www.members.co.jp/
制作を依頼する