KAKESU~/@dotbind-kondo/build-time-ogp-generation

記事ごとの OGP をビルド時生成に切り替えて Workers の CPU 制限を回避した

trail

前提実行時に描くしかなく、有料プランに上げるしか道はないと思っていた
観察別プロジェクトの実装はビルド時に生成していた。成立の理由はコンテンツがリポジトリにあること
前提こちらは DB にあるので移せない、と一度判断した(これが誤り)
観察D1 はビルド環境からも読める。DB にあるコンテンツでも同じ手が使える
却下公開時に 1 枚だけ描いて保存する案 — 描画自体が 10ms を超えるので Free では生成できない
却下有料プランに上げる案 — 無料のまま解けるなら要らない
却下実行時の実装をフォールバックとして残す案 — 同じ描画を 2 つ持つと必ずずれる
決定ビルド時生成に一本化し、実行時の描画は削除した
観察差分生成の印を配信ディレクトリに置いたら、配信物に混ざっていた
観察記事ごとにフォントを取ると、公開数ぶん往復が増える
決定印は配信の外へ、フォントは全件まとめて 1 度だけ取る形にした

背景

公開記事ごとの OGP 画像を、Worker が実行時に描いていた。動きはするが 1 枚あたり数十 ms の CPU を使い、Workers Free の 1 リクエスト 10ms に収まらない。そのため環境変数で既定 off にしてあり、実質使えていなかった。

SNS のカードは公開記事が人の目に触れる主な経路なので、共通画像のままだとどの記事も同じカードになる。

当初は「有料プラン($5/月)に上げるしかない」と判断していた。

調査

社内の別プロジェクト(名称は省略)で、同じ用途の実装がビルド時に動いていた。satori + resvg-js で PNG を作り、静的ファイルとして置く形。実行時の描画がゼロなので CPU 制限に当たらない。

ただしあちらはコンテンツがリポジトリの中(YAML)にあり、こちらは DB にある。最初は「ビルド時には記事が存在しないので移せない」と考えたが、これは誤りだった。

  • D1 はビルド環境からも読める。 REST API と wrangler d1 execute --remote がある
  • 静的アセットは Worker を通らずに配信されるので、配信時の CPU もゼロ
  • 生成済みのファイルがあると、Astro の SSR ルートより静的アセットが優先される

Cloudflare の制限も確認した。

  • Cron Trigger の CPU: Free は 10ms(実行時に描く案は、いつ描いても同じ壁に当たる)
  • Workers Builds のビルド枠: Free は 3,000 分/月、同時 1
  • Deploy Hook: URL への POST でビルドを起こせる。認証ヘッダ不要、Worker あたり毎分 10 回まで

判断

ビルド時生成に一本化し、実行時の描画は削除した。実装を 2 つ持つと必ずずれるため、残さなかった。/og/<id>.png のルートは、まだ生成されていない記事を共通画像へ逃がすだけのものになった。

作りで効かせた点:

  • フォントは全件ぶんをまとめて 1 度だけ取る。 記事ごとに取ると、公開が増えたぶんだけビルド中の往復が増える。ラスタライザは複数のフォントを受けられるので、URL に載らない長さのときは分割して渡す
  • 差分生成の印は配信ディレクトリの外に置く。 最初は画像の隣に置いてしまい、配信物に混ざっていた
  • 資格情報が無い環境では黙って飛ばす。 手元のビルドを、本番 DB なしでは通らない状態にしたくない

実測値:

時間
21 枚を全部生成3.2 秒
0 枚(据え置き)1.6 秒(DB 問い合わせ + フォント取得の固定費)

差し引き 1 枚あたり約 76ms。500 件で約 40 秒なので、ビルド枠には十分収まる。

差分生成が効くのは手元だけ。 ビルドごとに新しいコンテナなので、キャッシュに置いた印は残らず、毎回全件を作り直す。

Next Action

  • [ ] Deploy Hook を作って secret に入れる(公開直後の記事に画像を付けたい場合)。無くても共通画像に逃げるだけで壊れない
  • [ ] 公開が数千件に増えたら、画像と印をオブジェクトストレージに置いてビルド間で残す

気づき

公開ページにはエッジキャッシュ(未ログインで 300 秒)が効いている。デプロイ直後に 5 分ポーリングしても古い HTML が返り、デプロイが失敗したように見えた。実際は最初から成功していた。公開ページのコードを変えたときは、最大 5 分は古い HTML が出る。