Cloudflare Workers で OG 画像を生成する

Overview

この記事では Cloudflare Workers 上で OG 画像の生成を試してみます。

Edge で実行できることで、オリジンサーバーへの負荷を軽減したり、地理的に近いところにリクエストが飛ぶので RTT が小さくなります。
また、Static Generation を行なっている場合、OG 画像の生成や画像の事前処理はとても重い作業なのでビルド時間が長くなってしまいます。これを Edge 上で行うことによってビルド時間の短縮につながります。さらに CDN Edge の場合、計算結果をキャッシュできるため、事前に Build した場合と比べて 2 回目以降のリクエストの処理スピードはそこまで変わらないはずです。

Cloudflare Workers とは

簡単に言えば CDN Edge で JavaScript などの独自の処理を書くことができるプラットフォームです。
インターフェースは Service Worker に似ています。

Edge 上で JavaScript のような言語を動かせることで、地理的に近いところから任意の処理を実行できます。
これにより、通信時間が削減されたり、自動的に負荷が分散されるのでパフォーマンスが非常に良いのが特徴です。

詳しい解説はいろいろなところに乗っているのでここではしません。詳しく知りたい方は以下の記事を参照してみてください。

料金について

Cloudflare Workers の料金を見るとかなり安いことがわかります。

主要なものだけ取り上げています。 Free Plan では以下の制限で使用できます。

  • 1 日あたり 10 万件のリクエストを含む
  • リクエストあたりの CPU 時間:10 ミリ秒以内(リクエストなどの非同期処理は含まない)
  • 最小遅延は最初のリクエスト後
  • script size: 1 MB

詳細は公式ドキュメントを確認してください。

セットアップ

セットアップ周りはwranglerという Cloudflare が作っている CLI がやってくれるので難しくないです。

一点詰まった点としては、wrangler.tomlaccount_idがセンシティブなものなのか分からず戸惑ってしまいました。
こちらの issueにある通り、account_idは公開されても問題ない ID のようでした。

そのほかの部分の詳細については公式ドキュメントを参照してください。

OG 画像を生成してみる

前提

Cloudflare Workers 上で OG 画像を生成するにあたりog_image_writerという自作のライブラリを使用してみます。これを使うことで改行処理や背景画像を設定したりできます。

og_image_writer は Rust で書かれており、wasm-packで生成される wasm をラップしてくれる JavaScript をそのまま npm で配信しています。

JavaScript 以外で扱いたい場合は、og_image_writerのコードから wasm を生成して使うことができます。

また Cloudflare Workers では Rust も動かすことができます。og_image_writerの crate(Rust の npm のようなもの)を読み込んでそのまま使用できます。

要件

OG 画像を生成するには以下の要件が必要です。

  1. wasm module を取得する
  2. font を取得する
  3. OG 画像を生成しレスポンスを返す

方法

順を追って上の条件を満たすように実装を進めてみます。

1. wasm modules を取得する

wasm を使用するには、 webpack で wasm をバンドルに含めてしまうか、wrangler.tomlに wasm_modules を指定する方法があります。

wranglerは Cloudflare Workers が用意してくれている CLI です。

今回は wasm modules に wasm ファイルを指定する方法で利用します。
wasm modules を利用するにはwrangler.tomlで wasm_modules に wasm ファイルのパスを指定します。

wasm_modules = { OG_IMAGE_WRITER_WASM = "./node_modules/og_image_writer/wasm_bg.wasm" }

og_image_writerではinit関数という wasm_modules をロードする関数を用意されています。この関数にOG_IMAGE_WRITER_WASMを渡して wasm を読み込みます。

await init(OG_IMAGE_WRITER_WASM);

2. font を取得する

font は外部のホストからfetchを使って取得します。

const font = await fetch(`https://example.com/font.ttf`)
.then((res) => res.arrayBuffer())
.then((buf) => new Uint8Array(buf));

og_image_writerには font をUint8Arrayとして渡さなければならないのでUint8Arrayに変換します。

font は滅多に変更されないデータなのでキャッシュの設定をしておくと効果的です。

Cloudflare Workers では fetch したリソースのキャッシュ方法を設定することができるので、リソースのキャッシュ方法を上書きしたい場合は使うと良さそうです。

3. OG 画像を生成する

次に OG 画像を生成します。

まずは簡単にog_image_writerの仕組みを説明します。

og_image_writerには 5 つの要素があります。

  1. window ... 基礎となる画像で、この window の上に text や image が描画されます。
  2. text ... 文字を描画するために使われます。
  3. textarea ... 文字を描画するために使われます。text と違う点としては、文字を小さな単位に区切って個別にスタイルを当てることができます。
  4. image ... 画像を描画するために使われます。
  5. container ... window を window 画像の上に描画するための要素です。window を 1 つの box として扱うことができます。

今回は window と text を使います。

まずは window を作成します。window を作成するにはOGImageWriter.new()を使用します。
また、window のスタイルを定義するためにWindowStyleを使用します。
WindowStyleで扱えるプロパティについてはドキュメントを参照してください。

const h = 630;
const w = 1200;

const windowStyle = WindowStyle.new();
windowStyle.height = h;
windowStyle.width = w;
windowStyle.background_color = Rgba.new(0, 0, 0, 255);
windowStyle.align_items = AlignItems.Center;
windowStyle.justify_content = JustifyContent.Center;
const window = OGImageWriter.new(windowStyle);

次に font を設定します。グローバルで共通な font 設定するためにFontContextを使用します。
FontContextに値を push すると window 全体で値が共有されるようになります。

clear したい場合はFontContext.clear()を使用します。

グローバルな font を設定せず直接 text や textarea 要素に font を指定できます。
注意点としては文字列を画像に描画したい場合、font がないとエラーになります。

const font = await fetch(`https://example.com/font.ttf`)
.then((res) => res.arrayBuffer())
.then((buf) => new Uint8Array(buf));

const fontContext = FontContext.new();
fontContext.push(mplus1);

次に text 要素を描画してみます。text 要素を描画するにはwindow.set_text()関数を使います。
また、text の要素のスタイルを定義するためにStyleを使用します。

Styleで扱えるプロパティはドキュメントを参照してください。

const titleStyle = Style.new();
titleStyle.font_size = 100;
titleStyle.color = Rgba.new(255, 255, 255, 255);
titleStyle.margin = Margin.new(0, 20, 0, 20);
titleStyle.max_height = h * 0.5;
titleStyle.word_break = WordBreak.BreakAll;
titleStyle.text_overflow = "...";
titleStyle.line_height = 2;
window.set_text("Hello OG Image", titleStyle);

最後に画像の端に著者名をつけてみます。

著者名はグローバルの font とは異なる font を使用したいのでwindow.set_text()の第 3 引数に指定します。

const usernameStyle = Style.new();
usernameStyle.font_size = 70;
usernameStyle.color = Rgba.new(255, 255, 255, 255);
usernameStyle.position = Position.Absolute;
usernameStyle.bottom = 50;
usernameStyle.right = 50;

const otherFont = await fetch(`https://example.com/other.ttf`)
.then((res) => res.arrayBuffer())
.then((buf) => new Uint8Array(buf));
window.set_text("author", usernameStyle, otherFont);

これらを描画するためにwindow.paint()を使用します。
これを実行しない場合、画像には何も描画されません。

window.paint();

最後に画像からデータを取り出してレスポンスとして返します。
データを取得するにはencodeを使用します。

const data = window.encode(
ImageOutputFormat.Jpeg,
ImageOutputFormatOption.new()
);
return new Response(data, {
headers: {
"content-type": `image/jpeg`,
},
});

これで下のような OG 画像が返ってくるはずです。

Lighthouse のスコアが全て 100 点

Performance

キャッシュがない時は 700ms ~ 1000ms 程で表示されます。キャッシュがある時は 200ms 程で表示されます。
worker で動かせるので Cloud Functions とかで動かすよりは速いはずです。

メリットと課題

OGP 生成の方法としては Headless browser で生成する方法と node-canvas で生成する方法があります。
これらとの Performance の比較はできていませんが、以下の点で有利な点があります。

  • wasm を動かせればどこでも動く
  • スタイル周りを CSS like な API で調整できる

一方で課題もあります。

  • まだまだスタイルできる幅が狭い
  • CSS like な API を提供していますがブラウザとの互換性を完全に実現できているわけではない

まとめ

初めて Cloudflare Workers を使ってみましたが簡単に利用できました。
開発環境も考慮されており、ローカルで開発できるように工夫されているので体験もよかったです。

og_image_writer については、まだまだ使える API が少なくデザイン的に細かい所までこだわるのが難しかったりします。
使いたい機能のリクエストなどあればissueまでお願いします。

余談
実は OG 画像の生成以外にも Squoosh を動かそうとしてみたのですが、まだ対応しておらず実装できませんでした。
この issueが解決すれば使えるようになるはずです。