Deno Deploy で無料画像最適化(avif対応)
Deno Deploy の Web Cache API サポート
2024/8/27 に Deno Deploy が Web Cache API をサポートしました。
https://deno.com/blog/deploy-cache-api
これにより、生成したコンテンツをキャッシュして高速に配信することができるようになりました。
画像最適化
画像の最適化は、Web サイトのパフォーマンスを向上させるために重要です。本来なら事前に最適な状態にしておくのが望ましいのですが、状況に応じてサイズや品質を調整しなければならないこともあります。しかし画像の変換は CPU を多く消費するため、時間のかかる処理です。そのため一度変換した画像をキャッシュすることによって、処理時間の短縮を図る必要があります。
Deno Deploy と Cloudflare Workers の無料枠の比較
画像最適化は Cloudflare 用の記事をこちらで書いています
https://next-blog.croud.jp/contents/161ec5f7-3b10-4ac4-9dd9-7fadf744a39c
https://next-blog.croud.jp/contents/aebd7b12-a070-4573-b8f1-600904a1ebbb
今回は Deno Deploy での画像最適化を行います。その前に、Deno Deploy と Cloudflare Workers の無料枠を比較してみます。
https://deno.com/deploy/pricing
https://developers.cloudflare.com/workers/platform/pricing/
重要なところだけ抜粋すると以下のようになります。
Deno Deploy | Cloudflare Workers | |
---|---|---|
Requests | 1,000,000/month | 100,000/day |
CPU | 50ms | 10ms |
Size | - | 1MB(圧縮時) |
無料枠のリクエスト数は Deno Deploy が月単位で、Cloudflare Workers が日単位です。どちらも普通に使うには十分な数が用意されています。ちなみに Vercel で画像最適化をやると無料枠は 1000/月です。
CPU に関しては、Cloudflare の 10ms はけっこうキツイです。2K 解像度をを Webp に変換しようとすると、かなりの確率で失敗します。デジカメで撮った写真をそのままアップロードしているようなケースでは問題が発生します。
サイズに関しては、プログラムコードを圧縮したときの値です。Deno Deploy は特に制限がないようです。Cloudflare の 1MB は、普通のコードなら何の問題もないのですが、画像変換に関しては wasm を使うことになるためそれなりにタイトです。そのためWorkers用の画像変換ライブラリには avif エンコーダを含めることが出来ませんでした。avif エンコーダは圧縮しても 1MB を超えます。
また、Cloudflare ではキャッシュ機能を利用するのにこちらはカスタムドメインの設定が必要ですが、Deno Deploy の Cache はドメインの設定は不要です。
画像変換プログラム
画像変換にはこちらのライブラリを使います。Cloudflare Workers 用に作ったものに avif エンコード機能を追加しています。
https://www.npmjs.com/package/wasm-image-optimization-avif
1import { optimizeImage } from "npm:wasm-image-optimization-avif/esm";23const isValidUrl = (url: string) => {4 try {5 new URL(url);6 return true;7 } catch (_e) {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)) ??18 true19 );20};2122Deno.serve(async (request) => {23 const url = new URL(request.url);24 const params = url.searchParams;25 const type = ["avif", "webp", "png", "jpeg"].find(26 (v) => v === params.get("type")27 ) as "avif" | "webp" | "png" | "jpeg" | undefined;28 const accept = request.headers.get("accept");29 const isAvif = isType(accept, "avif");30 const isWebp = isType(accept, "webp");3132 const cache = await caches.open(33 `image-${isAvif ? "-avif" : ""}${isWebp ? "-webp" : ""}`34 );3536 const cached = await cache.match(request);37 if (cached) {38 return cached;39 }4041 const imageUrl = params.get("url");42 if (!imageUrl || !isValidUrl(imageUrl)) {43 return new Response("url is required", { status: 400 });44 }4546 if (isAvif) {47 url.searchParams.append("avif", isAvif.toString());48 } else if (isWebp) {49 url.searchParams.append("webp", isWebp.toString());50 }5152 const cacheKey = new Request(url.toString());53 const cachedResponse = await cache.match(cacheKey);54 if (cachedResponse) {55 return cachedResponse;56 }5758 const width = params.get("w");59 const quality = params.get("q");6061 const [srcImage, contentType] = await fetch(imageUrl)62 .then(async (res) =>63 res.ok64 ? ([await res.arrayBuffer(), res.headers.get("content-type")] as const)65 : []66 )67 .catch(() => []);6869 if (!srcImage) {70 return new Response("image not found", { status: 404 });71 }7273 if (contentType && ["image/svg+xml", "image/gif"].includes(contentType)) {74 const response = new Response(srcImage, {75 headers: {76 "Content-Type": contentType,77 "Cache-Control": "public, max-age=31536000, immutable",78 },79 });80 cache.put(request, response.clone());81 return response;82 }8384 const format =85 type ??86 (isAvif87 ? "avif"88 : isWebp89 ? "webp"90 : contentType === "image/jpeg"91 ? "jpeg"92 : "png");93 const image = await optimizeImage({94 image: srcImage,95 width: width ? parseInt(width) : undefined,96 quality: quality ? parseInt(quality) : undefined,97 format,98 });99 const response = new Response(image, {100 headers: {101 "Content-Type": `image/${format}`,102 "Cache-Control": "public, max-age=31536000, immutable",103 date: new Date().toUTCString(),104 },105 });106 cache.put(request, response.clone());107 return response;108});
以下のような URL でアクセスすることで画像を最適化することが出来ます。サイズもパラメータで指定することが出来ます。Next.js の画像最適化とパラメータの互換性があるので next.config.js で Deno Deploy の対象アドレスを設定すれば、Vercel の画像最適化と同じように使うことが出来ます。
avifに変換+256px (12KB)
https://deno-image.deno.dev/?url=https://raw.githubusercontent.com/SoraKumo001/cloudflare-workers-image-optimization/master/images/test01.png&w=256
pngをavifに変換しているのを確認できます。二回目以降のアクセスにはキャッシュが使われるので13ms程度で結果が返ってきます。
まとめ
Deno Deploy の欠点はしばらくアクセスがなかった場合の Cold Starts で微妙に遅延が発生することです。それ以外はコードサイズに制限がないぶん Cloudflare よりも扱いやすいと言えます。今回 Web Cache API のサポートが追加されたことで、使い勝手が格段に良くなりました。ランタイムが Deno だからと敬遠せず、一度試してみてください。