画像を軽くしたいとき、いまは WebP が定番の選択肢です。同じ見た目でもPNGやJPEGよりファイルが小さくなりやすく、表示速度やCore Web Vitalsにも効きます。でも「どうやって変換するの?」「透明は保持されるの?」「古いブラウザは大丈夫?」あたりで手が止まりがちです。
この記事では、**PNG / JPEG を、アップロードせずブラウザの中だけで WebP に変換する(そして必要なら戻す)**方法を、Canvas と少しのPythonでまとめます。
ゴール:画像を選ぶだけで、透明を保ったまま WebP に変換して軽くできるようになること。
そもそも WebP はなぜ軽いのか
WebP は Google が作った画像フォーマットで、非可逆(lossy)と可逆(lossless)の両方を扱えます。
- 非可逆:JPEG のように情報を間引いて圧縮。写真系で強い。
- 可逆:PNG のように劣化なしで圧縮。イラストやロゴ、スクショ系で強い。
- アルファ(透明)対応:PNG のような透明を保ったまま、非可逆でも圧縮できる(PNGだと透明を残すと重くなりがち)。
ざっくり「JPEGの軽さとPNGの透明を、いいとこ取りできる」フォーマットだと思っておくと使いどころが分かりやすいです。
基本:Canvas で PNG/JPEG を WebP に変換する
ブラウザには画像を再エンコードする仕組みが標準で入っています。画像を Canvas に描いて、toBlob で image/webp を指定するだけです。
async function toWebP(file, quality = 0.8) {
// 画像を読み込む
const bitmap = await createImageBitmap(file);
// 同じサイズの canvas に描画
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
// WebP として書き出す(quality は 0〜1)
return await new Promise((resolve) => {
canvas.toBlob(resolve, "image/webp", quality);
});
}
呼び出し側はこれだけです。
input.addEventListener("change", async (e) => {
const file = e.target.files[0];
const webpBlob = await toWebP(file, 0.8);
console.log(`${file.size} → ${webpBlob.size} バイト`);
// ダウンロードさせる
const a = document.createElement("a");
a.href = URL.createObjectURL(webpBlob);
a.download = file.name.replace(/\.\w+$/, "") + ".webp";
a.click();
});
quality は非可逆のときの画質です。写真なら 0.75〜0.85 あたりが、見た目とサイズのバランスが取りやすいです。
透明(アルファ)を保持したいとき
ここがPNGユーザーの一番の関心事だと思います。結論、特別なことは不要です。
canvas.getContext("2d") は既定で透明を保持するので、上のコードで PNGの透明はそのまま WebP に引き継がれます。逆に「白背景で塗りつぶしたい」場合だけ、描画前に背景を敷きます。
// 透明を白で埋めたいとき(JPEG化の前処理などで有効)
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(bitmap, 0, 0);
なお toBlob の quality 指定は非可逆WebPとして書き出します。ブラウザのCanvas経由では可逆(lossless)WebPを明示的に選ぶAPIが無いため、「劣化ゼロで可逆圧縮したい」ときは後述のPython(Pillow)やツールを使うのが確実です。
WebP を PNG に戻す
「WebPで受け取ったけど、PNGしか受け付けない場所に貼りたい」——逆変換も同じ要領です。出力の MIME を image/png にするだけ。
async function webpToPng(file) {
const bitmap = await createImageBitmap(file);
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext("2d").drawImage(bitmap, 0, 0);
return await new Promise((r) => canvas.toBlob(r, "image/png"));
}
PNGは可逆なので、quality 引数は無視されます(指定しても効きません)。
複数ファイルをまとめて変換する
Canvasは1枚ずつの処理ですが、Promise.all で並べれば複数枚も手早く回せます。
const files = [...input.files];
const results = await Promise.all(files.map((f) => toWebP(f, 0.8)));
// results[i] が各ファイルの WebP Blob
大量に処理するとメモリを食うので、数百枚規模なら「1枚変換→保存→次へ」と逐次にした方が安定します。
つまずきやすいところ
-
対応ブラウザ。 WebPの表示は主要ブラウザで対応済みですが、
canvas.toBlob(..., "image/webp")での書き出し対応は環境差があります。未対応だと勝手にPNGで書き出されることがあるので、blob.typeがimage/webpか確認すると安全です。 - 写真は非可逆・イラストは可逆が基本。 ブラウザCanvasは非可逆WebPになりがちなので、ロゴやスクショで劣化を避けたいときはPillowやツール側で可逆指定を。
- 必ず小さくなるとは限らない。 元がすでに最適化済みPNGだと、WebP化しても大きくなる場合があります。変換後にサイズを比較して、増えていたら元を使う判断を入れると親切です。
- アニメーションPNG/GIF。 単純なCanvas変換では静止画になります。アニメーションWebPは別処理が必要です。
- EXIF。 Canvasを通すと位置情報などのメタデータは基本的に落ちます(プライバシー面ではむしろ利点になることも)。
Python でやる場合
サーバーやバッチで処理するなら Pillow が手軽で、可逆・非可逆も選べます。
from PIL import Image
img = Image.open("input.png")
# 非可逆(写真向け・quality 0〜100)
img.save("output.webp", "WEBP", quality=80)
# 可逆(イラスト・ロゴ向け・劣化なし)
img.save("output_lossless.webp", "WEBP", lossless=True)
透明を保ったまま変換したいときは、img.convert("RGBA") してから保存すると確実です。
まとめ
- WebP は「JPEGの軽さ」と「PNGの透明」を両取りできるフォーマット
- ブラウザだけなら Canvas +
toBlob("image/webp", quality)で変換できる - 透明は特別な処理なしで保持される(白埋めしたいときだけ背景を描く)
- 戻すのも同じ要領(出力MIMEを
image/pngに) - 可逆WebPで劣化ゼロにしたいときは Pillow(
lossless=True) が確実 - 変換後はサイズを比較して、増えていたら元を使うと安全
「画像をもう少し軽くしたい」に、WebP × ブラウザ完結はよく効きます。参考になれば嬉しいです。