空雲 Blog

Eye catch
Cloudflare Workersの64MiB緩和で実現するHTML画像変換&画像最適化(wasm-html-to-image)

publication: 2026/09/06
update:2026/09/06


はじめに🔗


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-optimization
  • satoru-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});
10
11// パターンB: 既存画像を渡す → AVIF / WebP への変換やリサイズを出力(画像最適化など)
12const optimizedImage = await render({
13 value: srcImageBinary, // Uint8Array または ArrayBuffer
14 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 モジュールをバンドルするため、rulesCompiledWasm を指定します。


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 preact
2pnpm 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";
5
6const 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 }
15
16 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";
21
22 // キャッシュの確認
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 }
29
30 // JSX で OGP の HTML レイアウトを定義
31 // Google Fonts の Noto Sans JP を読み込み
32 const html = toHtml(
33 <html className="m-0 p-0">
34 <head>
35 <link
36 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" />
44
45 <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 Content
51 </div>
52 </div>
53
54 <div className="text-[80px] font-black text-white leading-[1.1] mb-[30px] break-words flex">
55 {title}
56 </div>
57
58 <div className="text-[32px] font-normal text-[#94a3b8] leading-[1.4] flex">
59 {subtitle}
60 </div>
61 </div>
62
63 <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 <img
67 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-ogp
77 </div>
78 </div>
79 </div>
80 </body>
81 </html>,
82 );
83
84 // 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 });
92
93 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 });
104
105 ctx.waitUntil(cache.put(cacheKey, response.clone()));
106 return response;
107};
108
109export 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";
2
3const isValidUrl = (url: string) => {
4 try {
5 new URL(url);
6 return true;
7 } catch {
8 return false;
9 }
10};
11
12const isType = (accept: string | null, type: string) => {
13 return (
14 accept
15 ?.split(",")
16 .map((format) => format.trim())
17 .some((format) => [`image/${type}`, "*/*", "image/*"].includes(format)) ?? true
18 );
19};
20
21export 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")) as
26 | "avif"
27 | "webp"
28 | "png"
29 | "jpeg"
30 | undefined;
31
32 const accept = request.headers.get("accept");
33 const isAvif = isType(accept, "avif");
34 const isWebp = isType(accept, "webp");
35
36 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 }
42
43 const imageUrl = params.get("url");
44 if (!imageUrl || !isValidUrl(imageUrl)) {
45 return new Response("url is required", { status: 400 });
46 }
47
48 const width = params.get("w");
49 const quality = params.get("q");
50
51 // ソース画像を取得
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(() => []);
55
56 if (!srcImage) {
57 return new Response("image not found", { status: 404 });
58 }
59
60 // 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 }
71
72 // 出力フォーマットを決定(パラメータ指定優先、なければブラウザ対応状況から自動判別)
73 const format = type ?? (isAvif ? "avif" : isWebp ? "webp" : contentType === "image/jpeg" ? "jpeg" : "png");
74
75 // 画像のデコード、リサイズ、エンコードを 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 });
83
84 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 });
91
92 ctx.waitUntil(cache.put(cacheKey, response.clone()));
93 return response;
94 },
95};


ご覧のように、OGP 生成のコードと見比べても、呼び出しているのは 全く同じ render 関数 であり、オプションに渡す formatwidth などの指定方法も完全に同一です。


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";
2
3// マルチコアを活用して一括バッチ生成
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 動的配信や画像最適化パイプラインを検討されている方は、ぜひ試してみてください。