SSGサイトの開発手法 SSGビルドのボトルネックの見つけ方とその改善
静的サイトはユーザーのアクセスを高速にしてくれますが、ビルドを必要とします。時にこのビルドには時間がかかることもあり、開発やデブロイの負担となることがあります。そのようなボトルネックの見つけ方と改善方法を考えます。
はじめに
静的サイトジェネレーターを利用して、事前にWebサイト全体のHTMLファイルを用意する手法を採用すると、サイトの規模が大きくなるにつれてビルド時間が増加していくことがあります。もちろん、処理するコンテンツデータや書き出すファイルが増えるわけですから、ある程度は仕方のないことです。とはいえ、たとえば、ビルド時間が5分を超えるとなると、開発・運用時の大きなストレスになったり、ホスティングサービスの利用料が看過できないような事態にもなってきます。
この記事では、SSGビルドのプロセスをおさらいし、ピクセルグリッドでの事例も交えて、ビルド時間を短縮するための工夫を考えていきます。同じような状況に直面している方はもちろん、SSGやSSRなどの選択肢で迷っている方などにも参考になれば幸いです。
SSGをサポートするフレームワーク
SSGの基本的な部分は以下の記事も参考になるかもしれません。
- 「静的サイトジェネレーターについて考える座談会」シリーズ
- 「いまからわかるJamstack」シリーズ
- 「実践、Jamstackサイト制作」シリーズ
また、ピクセルグリッドではSSGのためのフレームワークとして、Eleventy、Astroをよく利用しています。その他にもNext.jsやGatsbyなどの選択肢もあるかもしれません。各フレームワークについては、公式のドキュメントや、CodeGridの記事検索もご活用ください。
ビルドの流れ
SSGのビルドと言っても、制作するWebサイトによっていろいろなパターンがありますが、おおよそ以下のような流れで多くのケースはカバーできるでしょうか(このプロセスのすべてを行うとは限りません)。
- ヘッドレスCMSサービスのAPIからコンテンツデータを取得
- あるいはローカルに保存されたMarkdownファイル、その両方などから取得
- 取得したデータの前処理
- ページのテンプレートにデータを流し込んでHTMLを生成
- 生成したHTMLの加工
- 生成したHTMLをファイルに書き出す
ローカル環境にフレームワークをインストールしてサイト制作を始めると、開発やビルド時にはおおよそ以上のような処理が行われます。次節からは、プロセスごとにビルド時間への影響を考えていきます。
コンテンツデータの取得
サイトのコンテンツデータは、ヘッドレスCMSサービスに保存されていたり、あるいはソースコードと同じところ(ローカルやGitHub上)にMarkdownなどのファイルとして存在することが多いでしょう。複数のサービスを組み合わせていることもあるかもしれません。ともあれ、データの取得経路としては、次の2つが想定されます。
- ネットワーク上のAPIにリクエスト
- ローカルのファイルを読み込む
このうち、ローカルファイルの読み込みの場合は、ストレスになるほどの時間がかかることはほとんどないでしょう。確認すべきは、APIにリクエストしているケースです。ヘッドレスCMSに10,000件の記事が登録されており、10,000ページを生成するケースを考えてみます。
このとき、10,000件をまとめて取得できるサービスは稀でしょう。多くは、APIへの1回のリクエストで100件まで、などの制限があり、10,000記事であれば100記事ずつ100回のリクエストをすることになります。1リクエスト1秒としても1分40秒かかりますから、この部分はボトルネックになり得ます。
開発初期段階では記事の数が少なく、問題に気づけないことも多いポイントです。サイトの設計やCMSサービスを選定する段階で、将来的に想定される記事数やAPIの制限を確認できれば理想的です。
データの前処理
CMSから取得したデータを、そのままHTMLテンプレートに流し込むのではなく、何かしらフィルタリングしたり、中身を加工することがあるかもしれません。この段階ではコンテンツデータはメモリ上にあり、コンテンツ内の文字列のフォーマットなどであれば10,000件のデータを1件ずつ順に処理しても数秒のはずです。ただし、次のような例では注意が必要です。
- 1件のデータを処理する際に、外部のAPIに追加のリクエストが必要
- 1件のデータを処理する際に、他の9,999件のデータを確認する必要がある
- データの処理にサードパーティライブラリを使っており、そのライブラリが重い
とはいえ、ここは主にJavaScriptでの処理が多い部分です。console.time()などを使って処理時間を計測すれば、どこがボトルネックになっているかの特定は比較的容易でしょう。SSG特有の事情は少なく、ループ処理の工夫による高速化など、JavaScriptの汎用的な知識も活かせる部分です。
テンプレートへの流し込み
データの前処理が終わったら、そのデータを元にHTMLを生成します。HTMLの元となるマークアップは、AstroであればAstroコンポーネントに記述されていますし、EleventyであればいくつかのHTMLテンプレート言語が選択できます。フレームワークにお任せ……に見えるところですが、テンプレート言語によって処理速度には差があります。
ページ数の増大が想定される場合、フレームワークが推奨しているものを第一の選択肢にする、あるいは、実際にベンチマークを取るなどして決めると良いです。
補足:採用する言語・ライブラリによるビルド速度の差
Eleventyは公式にテンプレートごとのベンチマークデータを公開しています。
また、AstroはViteでビルドをします。Viteのパフォーマンスについてのドキュメントも参考になるかもしれません。
生成したHTMLの加工
HTMLを生成してから加工する、という処理をしたいケースもあります。たとえば、HTML自体の圧縮や、見出しに自動的にアンカーリンクを挿入するなどの処理です。HTMLファイルのディレクトリ階層が深かったり、HTMLの構造が複雑だったりすると、全体に大きな、かつ深いツリー構造になっているかもしれません。プログラムが1ページずつHTMLを読み込んで、このツリー構造を解釈し、加工してからまたHTMLとして出力する、という処理になるため、加工前のコンテンツデータを処理するより時間がかかる可能性が高いです。
後からの加工は「HTMLになってからでないとできない」処理のみに限定する、という方針が筆者のおすすめです。
ファイルへの書き出し
最後に、生成したHTMLデータをファイルに書き出す部分です。ここも、ローカルファイルの読み出しと同様、たとえ10,000ファイルあってもそれほど時間はかからないでしょう。
ビルド時間の計測
ビルドのどの部分がボトルネックになっているのか、計測する方法を見ていきます。基本的には、ビルド中の要所要所でコンソールに時間を出力して見ていくことになります。
フレームワークが備えている機能
フレームワークにはビルド時の詳細なログを出力してくれる機能があります。その機能を使うことで、ボトルネックになっている処理がわかる場合があります。
Eleventyの場合、DEBUG環境変数をセットしてビルドを実行すると、詳細なログを出力してくれます。各処理にかかっている時間も一覧できるので、便利です。
Astroでも、npm run build -- --verboseのように--verboseオプションを追加してビルドすると、処理にかかった時間がコンソールに出力されます。また、configファイルでlogLevel: "debug"をセットしても同様の情報が得られます。まずは、フレームワークが提供している機能を使ってみるのが良いでしょう。
ホスティングサービスのログ
ホスティングサービス上の環境でビルドし、デプロイする際には、フレームワークのビルドプロセスの前に、Node.js環境の立ち上げや依存するnpmパッケージのインストールの処理が走ります。サービスにもよりますが、この部分に数分かかることもあります。
npmは、オプションや環境変数により、devDependenciesのインストールをスキップする機能を備えていますので、本番ビルドには不要なものはdependenciesではなくdevDependenciesに定義しておく、不要なライブラリは削除するなど、package.jsonの中身を一度確認してみても良いかもしれません。
ローカルとホスティングサービスでのビルド時間の違い
ホスティングサービス上の環境は、ローカルのコンピューターに比べてCPUやメモリなどのリソースが少ないことがほとんどです。そのため、ビルド時間もローカルより長くなると考えておくと良いでしょう。サービスにより、設定できるメモリの上限や、タイムアウトまでの時間が決まっていることもあるので、そのあたりもドキュメントで確認しておくと良いです。
実際の改善事例
ピクセルグリッドでも、運用していくうちにコンテンツが増え、ビルド時間の改善を行った事例があります。いくつかを紹介します。
APIからのデータ取得が遅かったケース
この事例のサービスではコンテンツ管理にPrismicを利用していました。Prismic自体はサービスと相性が良く、良い選択だったのですが、APIからのデータ取得に関しては、1リクエストあたり20件まで、という制限があります。記事数が2,000件になると100回のリクエストが必要で、そこだけで1分近くかかっていました。
差し当たっての解決策としては以下の2点が挙げられます。
- GraphQL APIは20件までだが、RESTful APIは100件までなので、変更を検討する
- ローカルでの開発時、オプションをつけて実行して全件のデータを取得しないようにする
後者の解決策に関しては、ホスティングサービス上でのビルドには影響しませんが、手元でHTMLテンプレートを少し編集するだけで1分待つ、というストレスはなくなりました。また、ホスティングサービスについても、GitHubにコードの差分をpushするたびにプレビュー環境のビルドが走らないよう、ビルドのトリガーを調整するなどの工夫も行いました。
開発時に全件のデータを処理しない
Eleventyは.eleventyignoreという名前のファイルや、設定ファイルへの記述により、ビルド時に無視するファイルやディレクトリを指定できます。また、CMSのAPIからの取得を最初の20件で切り上げるような処理を_dataディレクトリ内のJavaScriptファイルに記述しておき、環境変数によって切り替えるようにしたこともあります。
HTMLの後処理が重かったケース
こちらのケースでもEleventyで、addTransformフィルターを使い、テンプレートから出力された各HTMLを加工する処理を挟んでいました。具体的には、プラグインを使ってページ内で使われるJavaScriptのビルドと<script>タグの埋め込みをしていたのですが、ページ数が増えると数分単位の時間がかかるようになっていました。
補足:addTransform
addTransformはビルド後の最終出力ファイルに対して、任意の変換処理を適用するものです。
解決策としては、「ページ内で使われるJavaScript」というのは、大部分が各ページ共通のものだったので、ビルド結果のキャッシュが効くようなコードを自作し、プラグインを置き換えました。
サードパーティーのプラグインは便利なものではありますが、100記事程度のブログであれば問題ないが、10,000記事のサイトではボトルネックになる、ということもあります。プラグインがどのような規模を想定しているのか、どのくらいのボリュームまで検証済みか、導入前に確認しておけば、後から自作プラグインと置き換える事態は防げたかもしれません。
HTMLの後処理が遅かったケース
このケースでも先の例と同様、ページ数が増えると、ビルド時間が数分単位で増加してしまいました。フレームワークの機能で計測してみると、パンくずリストのナビゲーションを自動生成するプラグインのところで、数分かかっていることがわかりました。
プラグインの動作について改めて調べてみると、サイトの設計を見直すことで改善の余地はありそうでしたが、すでにサイトの運用が進んでいるこの時点から設計を変更するのは現実的ではありません。そこで、HTMLテンプレートごとにパンくずリストに必要な情報を埋め込むように変更し、プラグインが処理する部分を減らすことでビルド時間を短縮できました。
地道な改善作業ですが、サイトの設計や利用しているプラグインについて、ドキュメントなどで把握しやすくなっていれば作業の助けになります。
事例のまとめ
紹介してきた事例はどれも、サイトの構築から数年が経ってからビルド時間の改善に取り組んでいました。このような改善を現在、将来にわたって適切に行うために、当初の設計やプラグインの選定についての情報を残しておくことの重要性を感じました。
おわりに
事前にすべてのHTMLファイルを生成してしまうSSGのアプローチは、シンプルな仕組みで高速なWebサイト配信ができる点で大きなメリットがあります。実際にフレームワークやヘッドレスCMSのチュートリアルなどをやってみると、Webサイト構築の容易さを実感するかもしれません。
しかし、実際の制作現場では、そうして作ったサイトが継続的に安定稼働することが重要です。運用していく中では、お話ししたようなデータ量の増加や、機能の追加要望など、さまざまな事情が出てきます。そういった状況の解決策は個々のケースにより異なるもので、事前のシミュレーション・計測などの地道な作業や、フレームワークが行う処理の理解が欠かせません。
筆者は日頃、そのような問題解決を楽しんでいる面もあるのですが、この記事を通して、Webサイトを運用していく中ではこんなこともあるのだ、と少し具体的なイメージが伝わっていたらうれしく思います。