記事ごとの OGP をビルド時生成に切り替えて Workers の CPU 制限を回避した
trail
背景
公開記事ごとの 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 が出る。