Home JavaScript Greasemonkey PHP

いかに爆速ブログシステムを自作し WordPress を置き換えたか2022-12-22


自作のブログシステムに OTCHY.NET を移行しました」で書いたように、OTCHY.NET 公開時から 12 年以上の間、幾度かのバージョンアップやサーバの乗り換えを経てもずっと使っていた WordPress を、ついに完全自作のブログシステムで置き換えました。

ブログシステムを自作するにあたってはとことん速度にこだわって作ったので、ここではいかにして爆速ブログシステムを作ったかという話をします。なぜ今さらになってブログシステムを自作したかという話は前述のポストを読んで下さい。

静的サイトジェネレータ

そもそもシステムの自作を始めた理由が、WordPress で提供している全てのページを静的な HTML に置き換えたい、というものでした。したがって、作るシステムが静的サイトジェネレータになるのは大前提でした。爆速システムを作りたかったから静的サイトジェネレータを選択したというよりは、静的サイトジェネレータを作ることにした → ページの表示速度も速くなるはずだ → どうせなら爆速を目指そう、という事です。

ただ静的サイトジェネレータに関して一般的に言われることとして、動的な HTML 出力よりもレンダリングは速くなる一方で、特にページ数が多くなると、事前に HTML を出力する処理自体にかかる時間は長くなるというものがあります。これは静的サイトジェネレータが悪いというわけではなく、単にどのタイミングで時間を使うかというトレードオフの問題です。

でも言うて 10,000 ページとか記事があるわけでは無くせいぜい数百のオーダーなので、速度にこだわって作れば HTML 出力も遅くなるはずは無い、という肌感覚はありました。そこで自作システムでは HTML 出力もレンダリング速度も高速、という良いとこ取りを目指しました。以下、どういうポイントにこだわって高速化をしたかを解説していきます。

データベースを持たない

もともとの WordPress のデータは MySQL データベースに格納されているので、そこから直接データを取得して HTML を生成するという選択肢もありました。しかし、以下の理由からデータベースを持つ事に取り立ててメリットは無いと判断しました。

  • データベースにアクセスするにはその都度オーバーヘッドがある
  • 静的サイトジェネレータではどのみち全ページを毎回再生成するので、データベースが持つ多様で高速な検索機能が不要
  • 自分一人しか使わないので ACID 原則を気にする必要が無い

そこでデータベースから取得したデータをいったん全てテキストファイルとして保存するというプロセスをつくりました。それには以下のようなメリットがあります。

  • データを git で管理できる
  • 後々のデータ更新が早くて楽
  • 各ページに関するデータをなるべく一ヶ所にまとめることで、WordPress のテーブル設計上避けられない JOIN 句を多用した SELECT 文よりよりもパフォーマンス的に有利
    • 具体的には content.htmlmeta.json の 2 つのファイルだけで各ページの情報を管理

汎用的に作らない

WordPress のデータベース上に保存されていたデータをテキストファイルに落とし込む過程で WordPress の設定を読み込んで真面目に実装することで、汎用的な、誰でも使える WordPress の乗り換え先としての静的サイトジェネレータをつくる事もできました。世の中的にそんなニーズもあるだろうし、それで喜んでくれる人もいるだろうとも思いました。Git リポジトリ名の no-more-wordpress はそんなことを考えていた時の名残でもあります。MySQL のパスワードなどが保存される config.mjs.gitignore で隠蔽され、代わりに config.example.mjs がコミットされているのも、そう、その名残です。

しかし、汎用的な作りは常にスピードとトレードオフです。そこで WordPress の設定項目をデータベースから読み込む事は一切せず、各ポストや一覧ページの URL、そのコンテンツなどは完全に決め打ちのハードコードで、自分だけが満足するように実装しています。汎用的で無いが故にテストも楽で、開発スピードが上がるという副次的効果もありました。開発スピードが速いというのは、特に個人プロジェクトにおいては、自身のモチベーションを高く保つ上で重要ですね。

ライブラリの利用を最小限にする

黎明期から JavaScript を書いている身としては、近年の npm を通じて提供されるライブラリの充実っぷりには目を見張るばかりです。自分も自分しか使わないであろうツールを公開したりもしています。ところがいくら便利でも、大半の人気のあるライブラリは汎用的な作りになっていて、前述のようにパフォーマンス上は不利になることが多いです。また利用するライブラリが増えれば、単純にその分だけライブラリ自体の読み込みにも時間がかかります。そこで、ライブラリの利用は最小限に抑えました。

HTML/JS/CSS/SVG のミニファイ、Data URI のエンコード、Markdown のパース/HTML 化はさすがにライブラリに頼りましたが、せいぜいそのくらいです。楽ちん便利ユーティリティの類は使っていません。

また、最初は HTML を生成するロジックに半ば無意識で何らかのテンプレートライブラリを使おうとしました。しかしテンプレートライブラリというと、大抵は何らかの制御構文 (if/for/while など) がセットになっており、それらをパースする時間、実行する時間が重いです。JavaScript エンジンの上で別の言語を動かすようなものなので当たり前ですね。

一方、現代の JavaScript はテンプレートリテラルを持っており、かなり高い自由度で、しかも高速に動作します。そのことに気付いたので、HTML の出力にテンプレートライブラリは一切使わずに、愚直にテンプレートリテラルでそのまま書いています。実際の挙動としてはひたすら文字列を連結しているだけなのですが、読みやすい構文のお陰で苦も無く書くことができました。

小さく早いライブラリを選び何ならライブラリ自体をカスタマイズする

少ないながらも使うライブラリの選定には、なるべくサイズが小さく早いものを選ぶようにしました。これだけだとまあそれだけの話なのですが、それに加え、必要であればライブラリ自体をカスタマイズして高速化もしました。

これは JavaScript ではなく CSS 側の話になります。CSS に関しても一切ライブラリに頼らず全て自分で書くことも検討したのですが、ちゃんと複数のブレークポイントを持ったレスポンシブなデザインを自作するのはダルすぎた労力に対価が見合わないので、CSS フレームワークは利用する事に決め、サイズと速度に定評のある mini.css を選びました。

mini.css 自体、元々かなりスリムで余計なところの無いフレームワークで良いのですが、それでもやはり使っていくうちに、微調整で上書きしたくなるところ、まるっと不要な機能などがあることが分かりました。

実際に細かく調整したくなるところや変更頻度が多そうなところについては少量の上書き用 CSS で対応しているのですが、ガッツリと機能を削りたい部分については、mini.css をフォークして不要な部分を削った言わば ミニ mini.css を定義してビルドしています。結果として、CSS をフルスクラッチで自作していていたとしたら大体こんな感じだったろうと思うものに近いものができて満足しています。

抜かりなく最適化する

ビルドのスピードを極力早くするために、CSS や JS をミニファイした結果は全部メモリ上にキャッシュして再利用しています。これ、普通に考えると「そもそも元ファイルをミニファイするのはファイルごとに当然 1 回だけなのでは?」ってなると思います。ですが、CSS や JS をミニファイするユーティリティは、HTML テンプレートから呼び出すようにデザインされていて、ファイルのパスを受け取るとその内容を読んでミニファイした後、ミニファイ後のサイズが十分に小さければ HTML 内にインライン展開するような最適化も行っているため、ここでキャッシュが効いてきます。

また、同様の最適化を画像ファイルに対しても行っています。PNG / JPG のようなラスター画像はあらかじめ最適化しているためファイルサイズに応じたインライン展開だけですが、SVG の場合はミニファイとインライン展開の両方を同様のユーティリティで行っています。

そして最後に、当然 HTML もミニファイするわけです。

一方で、最適化を突き詰めていくとどうしてもトータルの HTML 出力時間は長くなっていってしまうので、ミニファイやインライン展開を一切行わない "Dev モード" も実装しています。これは、ブログの記事を書いている途中のプレビューで使用する事を想定しており、以前の記事で書いたように 1 ページあたり 1.5 ミリ秒以下の爆速ビルドを可能にしています。

非同期 I/O を多用する

「データベースを持たない」という選択をしたため、データのインプット元は全てただのファイルになります。また静的サイトジェネレータなので当然、データのアウトプット先もすべてただのファイルです。全ての記事データの集めるのにディレクトリを探索しないといけないし、各記事のメタデータ、サイト全体のメタデータもまたファイルに保存されています。そう、つまり何も考えずに実装すると、ビルド時のファイル I/O がすぐさまひどいボトルネックになってしまうのです。

これを避けるために非同期 I/O を多用しました。記事のコンテンツとメタデータが揃わないと HTML が出力できない、とか、ディレクトリの探索が終わってもファイルの出力がどこまで終わっているかは分からない、とか、同期で書いてたらもっと楽に記述できる所はありましたが、頑張りました。ことごとく非同期で書きました。

Node.js の API が全般的に非同期を前提としていること、JavaScript の async / await 構文にはだいぶ助けられましたね。

M1 Mac すげーわ

そして、結局物理で殴るのかよって話ではあるんですが、ちょうどこのブログシステムを作っていたのが自宅のメインマシンを初代の M1 Mac に買い替えたばかりの頃でして、ローカルでのビルドの早さの理由の一つに M1 Mac の早さを挙げないわけにはいきません。

CPU 自体の早さもさることながら、ビルトインされたメモリアクセスの早さ、専用にチューニングされた SSD の早さなど、その全てがビルドの早さに寄与していることは想像に難くないところです。今なら M2 Mac があるので、より高速なビルドが可能になっているのではないかと思います。

終わりに

そんなわけでつらつらとブログシステム高速化の話をしてきましたが、そもそもこのシステムの運用を始めてからすでに 1 年と 9 ヶ月も経っています。当初はなかなかの熱量を持ってこの記事を書き始めていたものの、途中で飽きて、書きかけのまま長期間寝かせてしまっていました。

現在のシステムに変わってから書いたブログ記事は、この記事を含めてなんとわずか 3 本!システム自体を書いている時間の方がよほど長いのは、エンジニアあるあるですかね…。

ちなみに今になってようやく書き上げられたのは、2022 年になってからずっと毎日コミットを続けており、コミットするネタ探しの中で重い腰を上げたからです。毎日コミットについては別記事にします。(今度こそ早く!)


カテゴリ: Development タグ: wordpress