Cloudflare Workersの64MiB緩和で実現するHTML画像変換&画像最適化(wasm-html-to-image)
- サンプルリポジトリ
https://github.com/SoraKumo001/wasm-html-to-image-samples - npm パッケージ
https://www.npmjs.com/package/wasm-html-to-image - 公式ドキュメント
https://sorakumo001.github.io/wasm-html-to-image/master/docs/
はじめに🔗
2026年9月4日、Cloudflare Workers に非常に大きなアップデートが行われました。Worker のデプロイサイズ制限が劇的に緩和され、これまでエッジ環境で動かすには重すぎた大規模な WebAssembly(WASM)モジュールをそのまま組み込めるようになりました。
https://developers.cloudflare.com/changelog/post/2026-09-04-increased-worker-size-limit/
これまで Cloudflare Workers 上で OGP 画像生成や画像最適化を行う際、「WASM のサイズ制限」 は常に最大の壁でした。特に AVIF のデコードエンジンや Skia 等の高機能な描画エンジンを同居させようとすると、制限サイズを超過してしまい、機能を削るか複数のライブラリに切り分ける必要がありました。
今回の制限緩和によりその制約が解消されたため、HTML からの画像レンダリング(Skia + litehtml)と画像フォーマット最適化(AVIF / WebP / JPEG / PNG / SVG / PDF 等の相互変換・圧縮)を 1 つに合体させた統合 WASM パッケージ wasm-html-to-image を作成・公開しました。
このライブラリの最大の特徴は、「HTML」を渡しても「画像データ」を渡しても、まったく同じ render 関数 1 つで指定したフォーマットの画像に変換・最適化できる 点にあります。
本記事では、Cloudflare のサイズ制限緩和のポイントと、新パッケージ wasm-html-to-image を使って Cloudflare Workers 上で OGP 生成や画像最適化を実装する方法について解説します。
Cloudflare Workers のサイズ制限緩和(2026年9月4日)🔗
従来の Cloudflare Workers では、Wrangler がバンドルしたコードを gzip 圧縮した後のサイズに対して制限が課されていました。
- 従来の制限:
- Free プラン: 圧縮後 3 MB
- Paid プラン: 圧縮後 10 MB
WASM ファイルはバイナリデータであるため gzip の圧縮が効きにくく、高機能な C++ / Rust 製ライブラリを持ち込もうとすると、無料プランの 3MB 枠はもちろん有料プランの 10MB 枠でもギリギリになるケースが多々ありました。
緩和後の新仕様🔗
今回の変更により、gzip 圧縮後のサイズチェックは完全に撤廃されました。代わりに「非圧縮時のバンドルサイズ(Uncompressed bundle size)」のみがチェックされるようになり、制限値は全プラン共通で 64 MiB に拡大されました。
| 項目 | 以前 | 現在 (2026-09-04 以降) |
|---|---|---|
| Free プラン | 圧縮後 3 MB | 非圧縮 64 MiB |
| Paid プラン | 圧縮後 10 MB | 非圧縮 64 MiB |
| 判定基準 | gzip 圧縮後サイズ | 非圧縮サイズ(Total Upload) |
現在のバンドルサイズは以下のコマンドで確認できます。
1wrangler deploy --outdir bundled/ --dry-run
出力例:
1Total Upload: 12.45 MiB / gzip: 4.21 MiB
Total Upload が 64 MiB 以内であればデプロイ可能です(gzip は参考値として表示されるのみで、制限の判定には使われません)。
これにより、従来は分割せざるを得なかった大規模な WASM バイナリや、組み込みフォント、複数の画像コーデックを躊躇なくデプロイできるようになりました。
これまでの課題と統合の背景🔗
これまで筆者は、エッジ環境での画像処理に向けていくつかのライブラリを公開・解説してきました。
satori+svg2png-wasm+wasm-image-optimizationsatoru-render(Skia + litehtml の WASM ポート)
以前のアーキテクチャでは、「HTML をパースしてベクター描画するエンジン」と「WebP や AVIF などの各種画像をデコード・エンコードする画像最適化ライブラリ」を別々に用意する必要がありました。
特に AVIF のデコードエンジン(libdav1d など)はライブラリ単体でもサイズが大きく、Skia 描画エンジンと一緒にまとめると無料プランの 3MB 上限に収まりませんでした。そのため、やむを得ずパッケージを分割したり、対応フォーマットを削るなどのトレードオフを強いられていました。
しかし、非圧縮 64 MiB まで許容されるようになったことで、HTML レンダリングエンジンと主要な画像コーデック群を単一の WASM モジュールに統合 することが可能になりました。
統合パッケージ wasm-html-to-image🔗
そうして誕生したのが wasm-html-to-image です。
このライブラリの最大の強みは、「HTML」を入れても「画像」を入れても、まったく同じ render 関数 1 つで指定したフォーマットの画像に変換できる 点にあります。
入力に応じたパイプラインの自動判定🔗
render({ value, format, ... }) を呼び出す際、入力となる value に何を渡すかによって、内部の処理パイプラインが自動的に切り替わります。
value に渡す入力 | 内部のパイプライン | 出力結果(format で指定) |
|---|---|---|
| HTML 文字列 ( <div... などのタグ文字列) | HTML レンダリング litehtml(パース)+ Skia(ベクター描画) | 指定フォーマットの画像にレンダリング(WebP / AVIF / PNG 等) |
| 画像データ ( Uint8Array / Buffer / URL) | 画像最適化・フォーマット変換 各画像コーデックによるデコード & リサイズ | 指定フォーマットの画像へ相互変換・圧縮(WebP / AVIF / PNG 等) |
呼び出し側のインターフェースは完全に共通です。
1// パターンA: HTML を渡す → WebP / PNG などの画像を出力(OGP 生成など)2const ogpImage = await render({3 value: `<div style="background: #1e293b; color: white; padding: 40px;">4 <h1>Hello World</h1>5 </div>`,6 width: 1200,7 height: 630,8 format: "webp",9});1011// パターンB: 既存画像を渡す → AVIF / WebP への変換やリサイズを出力(画像最適化など)12const optimizedImage = await render({13 value: srcImageBinary, // Uint8Array または ArrayBuffer14 width: 800,15 quality: 80,16 format: "avif",17});
これまでのように「HTML から画像を生成したいから Satori やレンダラーを導入する」「WebP や AVIF に圧縮したいから別個の画像最適化ライブラリを入れる」といった使い分けは一切不要です。すべて同じ render() 関数に投げるだけ で、意図した画像フォーマットが出力されます。
サポートする入出力🔗
- 入力形式:
- HTML 文字列(JSX からの変換 HTML、Tailwind クラス付き HTML など)
- Web サイト URL(
http://またはhttps://で直接フェッチして画像化) - 画像バイナリ(PNG, JPEG, WebP, GIF, AVIF, BMP の
Uint8Array/ArrayBuffer) - Data URL(
data:image/...;base64,...)
- 出力フォーマット (
format):webp: 高圧縮・高画質 WebP 画像avif: 次世代の超高圧縮 AVIF 画像png: 劣化のない PNG 画像jpeg: 標準的な JPEG 画像(quality指定可能)svg: ベクター描画ストリーム(HTML レンダリング時)pdf: ベクター PDF ドキュメントthumbhash: プレビュー用の超軽量プレースホルダー
- Cloudflare Workers (
workerd) にネイティブ対応:wasm-html-to-image/workerdを提供。Workers 特有の WASM モジュールバインドに最適化。- Preact / React JSX の HTML 化や、Tailwind CSS のユーティリティ抽出(
createCSS)も同梱。
Cloudflare Workers での実装🔗
実際のサンプルコード(リポジトリ: wasm-html-to-image-samples)をベースに、Workers 上での実装例を紹介します。
1. プロジェクト設定 (wrangler.jsonc)🔗
Wrangler の設定ファイルを用意します。WASM モジュールをバンドルするため、rules に CompiledWasm を指定します。
1{2 "$schema": "node_modules/wrangler/config-schema.json",3 "name": "cloudflare-ogp",4 "main": "src/index.tsx",5 "compatibility_date": "2025-02-04",6 "rules": [7 { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false },8 ],9 "observability": {10 "enabled": true,11 },12}
必要な依存関係をインストールします。
1pnpm add wasm-html-to-image preact2pnpm add -D wrangler typescript @cloudflare/workers-types
2. JSX + Tailwind による OGP 画像生成🔗
Preact の JSX でレイアウトを記述し、Google Fonts の Web フォントと Tailwind CSS を適用して PNG 画像を生成・配信する例です。
1/** @jsx h */2import { h, toHtml } from "wasm-html-to-image/preact";3import { render } from "wasm-html-to-image/workerd";4import { createCSS } from "wasm-html-to-image/tailwind";56const fetch = async (7 request: Request,8 _env: object,9 ctx: ExecutionContext,10): Promise<Response> => {11 const url = new URL(request.url);12 if (url.pathname !== "/") {13 return new Response(null, { status: 404 });14 }1516 const subtitle = url.searchParams.get("subtitle") ?? "subtitle";17 const title = url.searchParams.get("title") ?? "Title";18 const image =19 url.searchParams.get("image") ??20 "https://raw.githubusercontent.com/SoraKumo001/cloudflare-ogp/refs/heads/master/sample/image.jpg";2122 // キャッシュの確認23 const cache = await caches.open("satoru-cloudflare-ogp");24 const cacheKey = new Request(url.toString());25 const cachedResponse = await cache.match(cacheKey);26 if (cachedResponse) {27 return cachedResponse;28 }2930 // JSX で OGP の HTML レイアウトを定義31 // Google Fonts の Noto Sans JP を読み込み32 const html = toHtml(33 <html className="m-0 p-0">34 <head>35 <link36 href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap"37 rel="stylesheet"38 />39 </head>40 <body className="m-0 p-0">41 <div className="w-[1200px] h-[630px] flex relative bg-[#0a0a0c] overflow-hidden">42 <div className="absolute top-[-150px] right-[-150px] w-[600px] h-[600px] rounded-[300px] bg-[radial-gradient(circle,_rgba(79,70,229,0.3)_0%,_rgba(79,70,229,0)_70%)] flex" />43 <div className="absolute bottom-[-100px] left-[-50px] w-[400px] h-[400px] rounded-[200px] bg-[radial-gradient(circle,_rgba(168,85,247,0.2)_0%,_rgba(168,85,247,0)_70%)] flex" />4445 <div className="flex flex-row w-full h-full p-[60px] items-center justify-between z-10">46 <div className="flex flex-col w-[60%]">47 <div className="flex items-center mb-5">48 <div className="w-10 h-1 bg-[#6366f1] mr-[15px] rounded-sm" />49 <div className="text-2xl font-bold color-[#818cf8] tracking-widest uppercase flex">50 Featured Content51 </div>52 </div>5354 <div className="text-[80px] font-black text-white leading-[1.1] mb-[30px] break-words flex">55 {title}56 </div>5758 <div className="text-[32px] font-normal text-[#94a3b8] leading-[1.4] flex">59 {subtitle}60 </div>61 </div>6263 <div className="flex w-[35%] relative justify-center items-center">64 <div className="absolute w-[420px] h-[420px] rounded-[40px] border border-white/10 bg-white/3 rotate-[-3deg] flex" />65 <div className="w-[400px] h-[400px] rounded-[32px] overflow-hidden border-4 border-white/10 flex">66 <img67 className="w-full h-full object-cover"68 src={image}69 alt=""70 />71 </div>72 </div>73 </div>74 <div className="absolute bottom-10 left-[60px] flex items-center z-20">75 <div className="px-4 py-2 bg-white/5 border border-white/10 rounded-[10px] text-lg text-[#e2e8f0] font-medium flex">76 cloudflare-ogp77 </div>78 </div>79 </div>80 </body>81 </html>,82 );8384 // HTML + Tailwind CSS から PNG 画像をレンダリング85 const png = await render({86 value: html,87 css: await createCSS(html),88 width: 1200,89 height: 630,90 format: "png",91 });9293 const response = new Response(png.data as BodyInit, {94 headers: {95 "Content-Type": "image/png",96 "Cache-Control": "public, max-age=31536000, immutable",97 date: new Date().toUTCString(),98 },99 cf: {100 cacheEverything: true,101 cacheTtl: 31536000,102 },103 });104105 ctx.waitUntil(cache.put(cacheKey, response.clone()));106 return response;107};108109export default {110 fetch,111};
ポイント🔗
toHtml(...): Preact JSX をそのまま HTML 文字列へ変換します。createCSS(html): HTML 内で使われている Tailwind のユーティリティクラスを解析し、最小限の CSS を自動生成します。render(...): HTML・CSS・外部フォント・外部画像を統合処理し、ヘッドレスブラウザ不要で高品質な画像をエッジ上でレンダリングします。
3. リサイズ&画像フォーマット最適化(AVIF / WebP)🔗
ここでも使うのは、先ほどの OGP 生成とまったく同じ render 関数 です。value に HTML ではなく画像バイナリ(ArrayBuffer / Uint8Array)を渡すだけで、自動的に画像最適化パイプラインへと切り替わり、画像リサイズ&フォーマット変換プロキシ として動作します。
ブラウザの Accept ヘッダーを判別して、AVIF や WebP へ自動変換・圧縮配信する例です。
1import { render } from "wasm-html-to-image/workerd";23const isValidUrl = (url: string) => {4 try {5 new URL(url);6 return true;7 } catch {8 return false;9 }10};1112const isType = (accept: string | null, type: string) => {13 return (14 accept15 ?.split(",")16 .map((format) => format.trim())17 .some((format) => [`image/${type}`, "*/*", "image/*"].includes(format)) ?? true18 );19};2021export default {22 async fetch(request: Request, _env: object, ctx: ExecutionContext): Promise<Response> {23 const url = new URL(request.url);24 const params = url.searchParams;25 const type = ["avif", "webp", "png", "jpeg"].find((v) => v === params.get("type")) as26 | "avif"27 | "webp"28 | "png"29 | "jpeg"30 | undefined;3132 const accept = request.headers.get("accept");33 const isAvif = isType(accept, "avif");34 const isWebp = isType(accept, "webp");3536 const cache = await caches.open(`img-${isAvif ? "-avif" : ""}${isWebp ? "-webp" : ""}`);37 const cacheKey = new Request(url.toString());38 const cachedResponse = await cache.match(cacheKey);39 if (cachedResponse) {40 return cachedResponse;41 }4243 const imageUrl = params.get("url");44 if (!imageUrl || !isValidUrl(imageUrl)) {45 return new Response("url is required", { status: 400 });46 }4748 const width = params.get("w");49 const quality = params.get("q");5051 // ソース画像を取得52 const [srcImage, contentType] = await fetch(imageUrl, { cf: { cacheKey: imageUrl } })53 .then(async (res) => (res.ok ? ([await res.arrayBuffer(), res.headers.get("content-type")] as const) : []))54 .catch(() => []);5556 if (!srcImage) {57 return new Response("image not found", { status: 404 });58 }5960 // SVG や GIF はそのまま返却61 if (contentType && ["image/svg+xml", "image/gif"].includes(contentType)) {62 const response = new Response(srcImage, {63 headers: {64 "Content-Type": contentType,65 "Cache-Control": "public, max-age=31536000, immutable",66 },67 });68 ctx.waitUntil(cache.put(cacheKey, response.clone()));69 return response;70 }7172 // 出力フォーマットを決定(パラメータ指定優先、なければブラウザ対応状況から自動判別)73 const format = type ?? (isAvif ? "avif" : isWebp ? "webp" : contentType === "image/jpeg" ? "jpeg" : "png");7475 // 画像のデコード、リサイズ、エンコードを WASM で一括実行76 const { data } = await render({77 value: srcImage,78 width: width ? Number(width) : 0,79 quality: quality ? Number(quality) : undefined,80 format,81 speed: 9,82 });8384 const response = new Response(data, {85 headers: {86 "Content-Type": `image/${format}`,87 "Cache-Control": "public, max-age=31536000, immutable",88 date: new Date().toUTCString(),89 },90 });9192 ctx.waitUntil(cache.put(cacheKey, response.clone()));93 return response;94 },95};
ご覧のように、OGP 生成のコードと見比べても、呼び出しているのは 全く同じ render 関数 であり、オプションに渡す format や width などの指定方法も完全に同一です。
HTML レンダリングと画像最適化の内部パイプラインが完全に抽象化されているため、呼び出し側は「元のデータが HTML か画像か」を意識することなく、統一された API で出力画像をコントロールできます。
これまで Cloudflare Workers ではデコーダーのサイズ都合で難しかった AVIF への変換・圧縮(および AVIF ソースの取り込み) も、64 MiB 枠のおかげで極めてスムーズに実現できます。
サンプルプロジェクトと他環境への展開🔗
今回作成したサンプルコードは、GitHub リポジトリ wasm-html-to-image-samples にて公開しています。
リポジトリ内には Cloudflare Workers 向けの実装だけでなく、多様なフレームワーク・ランタイムでの実装例が含まれています。
cloudflare-ogp: 本記事で紹介した Cloudflare Workers 上での JSX + Tailwind OGP 生成cloudflare-image-optimization: Cloudflare Workers 上での AVIF/WebP 自動画像変換プロキシnext-image-convert: Next.js 16 (App Router) でのクライアントサイド画像変換react-router-image-convert: React Router v7 + Vite でのクライアントサイド画像変換node-image-convert: Node.js CLI によるローカルバッチ画像変換playground: ブラウザ上で HTML 編集と画像変換を即時試せる React 19 + Vite アプリ
マルチスレッド対応(Worker Pool)🔗
ブラウザや Node.js 環境では、wasm-html-to-image/workers を使用することでマルチスレッド(Web Worker プール)による高速な並列バッチ処理も可能です。
1import { render } from "wasm-html-to-image/workers";23// マルチコアを活用して一括バッチ生成4const results = await Promise.all(5 items.map((item) =>6 render({7 value: `<h1>${item.title}</h1>`,8 width: 1200,9 height: 630,10 format: "webp",11 }),12 ),13);
まとめ🔗
Cloudflare Workers のデプロイ制限が「非圧縮 64 MiB」へと引き上げられたことで、エッジコンピューティングにおける WASM の可能性は飛躍的に広がりました。
- サイズ制限の呪縛からの解放: 圧縮後 3MB/10MB を気にして WASM モジュールを分割・削減する必要がなくなった。
- HTML レンダリングと画像最適化の合体:
wasm-html-to-imageにより、Skia + litehtml の高精度な HTML 画像化と、AVIF/WebP などの高度な画像圧縮処理が 1 パッケージで完結。 - 統一された
renderインターフェース: HTML 文字列でも画像バイナリでも、まったく同じrender()関数に渡すだけで指定フォーマットの画像を出力可能。ユースケースごとに別ライブラリや別関数を使い分けるストレスから解放されます。 - ヘッドレスブラウザ不要: 重厚な Chromium などを動かすことなく、ミリ秒単位のエッジレスポンスで動的な OGP や最適化画像を提供可能。
Cloudflare Workers での OGP 動的配信や画像最適化パイプラインを検討されている方は、ぜひ試してみてください。