RemixのResource RouteでOGP画像を生成する際にハマったWASM問題とその解決法

はじめに
workers-og をインストールしてRemixのResource RouteでOGP画像を生成しようとすると、ローカルでは動くのにWranglerビルドで Could not resolve "workers-og" エラーが出ます。vite.config.ts の ssr.external: ["workers-og"] を追加することで解決できます。
この記事をお勧めしない人
- ローカル開発環境と本番環境で動作が異なることを、当然のこととして受け入れられる人。
- エラーメッセージを読まずに、Stack Overflowで解決策を探し続けることに時間を使える人。
- Viteの
ssr.external設定の意味を理解する必要性を、全く感じていない人。
もし一つでも当てはまらないなら、読み進める価値があるかもしれません。
ビルドエラーを放置すると
- デプロイ直前にエラーを発見し、OGP実装を一時的にリバートするリリース判断を迫られる。
- 別のCloudflare Workers専用パッケージを追加するたびに同じエラーが再発し、都度調査が必要になる。
workers-ogの内部でyoga-wasm-webが使われており、WASMファイルをバンドラーが処理しようとして競合する構造が理解できないと、代替ライブラリを試しても解決しない。
`ssr.external` という明るい未来
- この記事を読めば、Cloudflare Workers専用ライブラリをバンドル対象から除外し、ランタイムに委譲する設計思想が手に入る。
- 具体的には、
ssr.external: ["workers-og"]とexternalConditions: ["workerd", "worker"]の組み合わせで解決する設定を手に入れられる。 - この方法は、このブログのOGP画像生成で実証済みで、ビルドエラーをゼロにした。
私も同じでした
このブログでOGP実装時に Could not resolve "workers-og" と Could not load yoga-ZMNYPE6Z.wasm の2つのエラーが同時に出ました。エラーメッセージ自体に Add "workers-og" to the "external" option というヒントが書かれており、vite.config.ts に ssr.external: ["workers-og"] を追加することで解決しました。
📝 概要
Web標準に基づくフレームワークと最新のエッジ環境を組み合わせてOGP画像を動的生成する機能を実装した際、ローカル開発環境では正常に動作するのに、本番ビルドでエラーが発生するという典型的な環境依存問題に遭遇しました。
エラーメッセージは一見複雑でしたが、原因はシンプルで、解決策も明快でした。この記事では、エラーの発見から原因特定、そして解決に至るまでのプロセスを記録します。
発生環境の特徴
- アーキテクチャ : モダンフレームワーク + エッジランタイム
- 画像生成 : サーバーサイドでの動的生成
- 環境差異 : ローカル: 動作、本番ビルド: ❌ エラー
⚠️ 問題の発見と症状
ローカル開発サーバーをエッジ環境のエミュレータで起動すると、ビルドエラーが発生しました。
症状の整理
- ローカル開発サーバー: 正常動作
- ❌ エッジ環境エミュレータ: エラー
- ❌ 本番ビルド: ビルドエラー
重要な気づき:
Node.js環境の開発サーバーでは動作するが、エッジランタイム環境向けのビルドでは失敗するという環境依存の問題でした。
🔍 原因の絞り込みプロセス
複数の仮説を立てて検証しました:
- パッケージのインストール問題 : 依存関係が正しくインストールされているか確認
- ビルドツールの設定不足 : バンドラーの設定を見直し
- エラーメッセージの精読 : エラーが示唆する解決策を分析
調査の結果、エラーメッセージが重要なヒントを含んでいることに気づきました。
💡 根本原因の特定
調査の結果、以下の問題が根本原因であると特定しました。
画像生成ライブラリの内部構造
使用した画像生成ライブラリは、内部でレイアウトエンジンを利用しており、そのエンジンがWebAssembly (WASM)バイナリを含んでいました。
問題の核心:
- ライブラリがWASMファイルを含む
- バンドラーがデフォルトでWASMファイルを通常のモジュールとして処理しようとする
- しかし、エッジランタイムはWASMを特殊な方法で扱う必要がある
- バンドラーの処理とエッジランタイムのWASMローディングが競合
なぜローカルでは動くのか?
Node.js環境の開発サーバーはWASMファイルを直接読み込めます。しかし、エッジランタイム環境ではWASMの読み込み方法が異なるため、同じコードが動作しません。
環境分離の設計方針
この問題を解決するため、「バンドル対象から除外する」というアプローチを採用しました。
具体的には、以下の戦略でエッジランタイム専用ライブラリを扱います:
- バンドラーからの除外 : ビルドツールの設定でバンドルさせず、ランタイムに任せる
- 環境条件の明示 : エッジランタイム用のエクスポートを選択
- 段階的検証 : 各環境で段階的に動作確認
このアプローチにより、単に「エラーを消す」のではなく、 エッジランタイム環境の制約を理解した上での構造的な解決 を実現しました。
達成した成果
| 改善項目 | Before | After |
|---|---|---|
| ビルド結果 | エッジ環境でビルドエラー | 正常にビルド完了 |
| 環境互換性 | ローカルと本番で挙動が異なる | 両環境で一貫した動作 |
| WASM読み込み | バンドラーの処理と競合 | エッジランタイムに委譲 |
その結果、 「ローカルで動くのに本番で動かない」という環境依存問題を、ビルド設定の調整で解消する ことに成功しました。
AIに「ビルドエラーを解決して」と頼むと、高確率で以下のような提案が返ってきます:
- 「とりあえず別のライブラリを試しましょう」
- 「バンドラーの設定を変更してみましょう」
- 対症療法を繰り返す
しかし、これは 対症療法 です。一時的にエラーは消えますが、「なぜエッジランタイム専用ライブラリはバンドル対象から除外すべきか」という原理が欠けているため、同じ轍を踏み続けます。
根本原因は、 バンドラーの処理とエッジランタイムのWASM読み込みが競合する ことにありました。バンドラーは「すべてをバンドルする」ことが得意ですが、エッジランタイムは「ランタイムが提供する特殊な機能」を活用する必要があります。
ここから先は、AIが絶対に提案しない 「ssr.external設定パターン」という解決策 の全貌と、具体的なvite.config.tsの記述、externalConditionsの意味、使用したライブラリの実名、そして段階的検証のチェックリストを、すべて公開します。
この設定と検証手順をコピーすれば、エッジランタイム専用ライブラリの環境依存エラーループを回避し、 初回から本番環境でも動くビルド設定 を実現できます。私が実践で確立したssr.external設定のパターンと、実装済みのvite.config.ts構成を、ここで全て公開します。
🔧 解決策: ビルド設定の調整
では、実際に私が追加したビルドツール設定の具体的な記述と、各オプションの意味、そして使用したライブラリの実名、エッジランタイム専用パッケージの一般的なパターンを公開します。
この設定をそのままコピーすれば、「どのライブラリをexternalにすべきか」「externalConditionsとは何か」「具体的なエラーメッセージの解読方法」を毎回調べることなく、 再現可能な環境分離設定 を実現できます。また、なぜssr.externalが必要なのか、段階的検証の各ステップで何を確認すべきか、そして今後同じ問題に遭遇したときの判断基準も解説します。
使用した技術スタック
- フレームワーク : Remix v2 + Vite
- ホスティング : Cloudflare Pages/Workers
- 画像生成ライブラリ : workers-og
- 内部依存 : satori, yoga-wasm-web
発生したエラーメッセージ
Wrangler(Cloudflare Workers エミュレータ)でビルドすると、以下のエラーが発生しました:
X [ERROR] Could not resolve "workers-og"
app/routes/ogp.$slug[.png].tsx:5:31:
5 │ import { ImageResponse } from 'workers-og';
╵ ~~~~~~~~~~~~
The package "workers-og" wasn't found on the file system but is built into node.
Add "workers-og" to the "external" option to exclude it from the bundle, which
will remove this error and leave the unresolved path in the bundle.さらに詳細なエラー:
X [ERROR] Could not load yoga-ZMNYPE6Z.wasm (imported by /__vite-browser-external:yoga-wasm-web):
Reading from "yoga-wasm-web" is not handled by any pluginworkers-ogの内部アーキテクチャ
workers-ogは以下のライブラリを使用しています:
workers-og
└─ satori (SVG生成)
└─ yoga-wasm-web (レイアウトエンジン)
└─ yoga.wasm (WebAssembly バイナリ)この依存関係が、WASM読み込みの競合を引き起こしていました。
vite.config.tsの修正
エラーメッセージのヒントに従い、ssr.external設定を追加しました。
export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
tsconfigPaths(),
],
ssr: {
resolve: {
externalConditions: ["workerd", "worker"],
},
+ external: ["workers-og"],
},
});設定の意味
ssr.external: ["workers-og"]
- Viteに
workers-ogパッケージをバンドル対象から除外するよう指示 workers-ogはCloudflare Workersのランタイムが提供するものとして扱う- WASMファイルの読み込みはCloudflare Workersに任せる
externalConditions: ["workerd", "worker"]
- Cloudflare Workers環境(workerd)用のパッケージ解決条件を指定
workers-ogがCloudflare Workers用のエクスポートを選択できるようにする
🎓 学んだこと・まとめ
技術的な学び
エラーメッセージを丁寧に読む重要性
- エラーメッセージに解決策が書かれていることが多い
- 焦らず、一語一句確認することが大切
開発環境と本番環境の違いを理解する
- Vite開発サーバー : Node.js環境、柔軟
- Cloudflare Workers: V8 Isolates、制約あり
- ローカルで動いても本番で動かないのは、環境の違いが原因
プラットフォーム専用ライブラリの扱い方
- Cloudflare Workers専用ライブラリは、バンドラーから除外するのが定石
ssr.external設定でランタイムに任せる
WASMの扱いは環境依存
- Cloudflare Workersは事前コンパイル済みWASMのみサポート
- 動的WASM生成は禁止
- ランタイムがWASMを適切に読み込む仕組みを壊さない
- この制約はShikiのシンタックスハイライトでも同じ問題として現れました。あちらはビルド時HTMLに全変換することで解決しています。また、Cloudflare Pagesへのデプロイでfs系API全般が使えない問題もこの制約の大きな文脈に位置します。
今後のベストプラクティス
エラーメッセージは宝の山
- エラーメッセージに解決策が書かれていることを意識する
- Stack Overflowに飛ぶ前に、まずエラーを丁寧に読む
段階的なデバッグアプローチ
- ローカル開発(Vite)で動作確認
- Wranglerで動作確認
- ビルドで動作確認
- デプロイで動作確認
- 各段階で問題を切り分けることで、原因特定が容易に
Cloudflare Workers専用ライブラリのパターン
// vite.config.ts のパターン ssr: { external: [ "workers-og", // OGP画像生成 "@cloudflare/ai", // Cloudflare AI // その他のWorkers専用パッケージ ], }
