背景
画像を Data URL に変換するツールを作っています。
画像ファイルを選ぶと、<img src="data:image/png;base64,..."> や CSS の background-image にそのまま貼れる文字列が出てくるものです。
Data URL は、ファイルの中身を URL の中に直接書き込む形式だ。
仕様は RFC 2397 にあります。
https://www.rfc-editor.org/rfc/rfc2397
ツールには「Data URL にすると約33%増えます」という注意書きを出しています。
Base64 は3バイトを4文字で表すので、4÷3 で約1.33倍、という理屈です。
ただ、HTML や CSS は gzip で圧縮して配信されることがほとんどです。
gzip は、繰り返しの多いデータを小さくする圧縮方式で、Web サーバーが自動でかけていることが多い。
「33%増える」は、gzip の後でも本当なのか…
気になったので測ってみました。
結論
PNG のようにすでに圧縮されている画像は、gzip 後にはほぼ元の大きさに戻ります。
増えるのは、むしろ SVG のような文字のファイルのほうでした。
SVG を埋め込むなら Base64 ではなく、パーセントエンコード(< を %3C のように書く形)の Data URL にすると、gzip 後の増え方はかなり小さくなります。
測り方
ツール本体はブラウザの FileReader.readAsDataURL で変換していて、出てくる文字列は data:{MIMEタイプ};base64,{Base64の本文} の形です。
同じ形の文字列を Node.js で作り、元のファイル・Data URL・それぞれを gzip したものの大きさを比べました。
下記がそのスクリプト(measure.js)です。
引数に画像ファイルのパスを並べて渡すと、1ファイル1行で結果を出します。
const fs = require('node:fs');
const zlib = require('node:zlib');
const path = require('node:path');
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp' };
for (const f of process.argv.slice(2)) {
const buf = fs.readFileSync(f);
const dataUrl = 'data:' + MIME[path.extname(f).toLowerCase()] + ';base64,' + buf.toString('base64');
const gzRaw = zlib.gzipSync(buf, { level: 9 }).length;
const gzUrl = zlib.gzipSync(Buffer.from(dataUrl), { level: 9 }).length;
console.log(path.basename(f), 'raw', buf.length, 'dataUrl', dataUrl.length,
'gzip(raw)', gzRaw, 'gzip(dataUrl)', gzUrl, 'ratio', (gzUrl / buf.length).toFixed(3));
}
ratio は「Data URL を gzip したもの」が「元のファイル」の何倍か、です。
ブラウザが実際に受け取るのは gzip 後の HTML なので、この値で比べました。
実測
Node.js v24.11.1 で、手元にあった4つの画像を渡しました。
写真っぽい PNG、スクリーンショットの PNG、295 バイトの小さな PNG、3,005 バイトの SVG です。
$ node measure.js photo-noisy-640x420.png chmod.png hero-texture.png og-image.svg
photo-noisy-640x420.png raw 256586 dataUrl 342138 gzip(raw) 256569 gzip(dataUrl) 256389 ratio 0.999
chmod.png raw 100053 dataUrl 133426 gzip(raw) 98018 gzip(dataUrl) 99050 ratio 0.990
hero-texture.png raw 295 dataUrl 418 gzip(raw) 293 gzip(dataUrl) 359 ratio 1.217
og-image.svg raw 3005 dataUrl 4034 gzip(raw) 1097 gzip(dataUrl) 1787 ratio 0.595
Data URL の段階では、どれもきっちり約1.33倍に増えています。
ところが gzip 後を見ると、写真の PNG は 256,586 バイトが 256,389 バイトで、ほぼ同じです。
理由は、Base64 の1文字が6ビットぶんの情報しか持っていないことだと思います。
1文字8ビットのうち2ビットは余りなので、gzip の符号化がその余りをほぼ取り返してくれる。
PNG の中身はもともと圧縮済みで、それ以上は縮まないので、結果として元の大きさに戻るわけです。
小さな PNG は 1.217 倍でした。
data:image/png;base64, という22文字の前置きが、295 バイトに対しては無視できない重さになります。
SVG だけは増えたまま残る
問題は SVG です。
SVG はただの文字のファイルなので、元のままなら gzip で 3,005 バイトが 1,097 バイトまで縮みます。
Base64 にすると、同じタグの繰り返しがバラバラの文字列になって、gzip が繰り返しを見つけられなくなる。
gzip 後は 1,787 バイトで、元の SVG を gzip した 1,097 バイトの1.6倍です(上の ratio は gzip していない元ファイルとの比なので 0.595 になっています)。
下記は、同じ SVG をパーセントエンコードの Data URL にした場合です。
measure.js と同じく fs と zlib を読み込んだうえで動かしました。
const svg = fs.readFileSync('og-image.svg', 'utf8');
const gz = (s) => zlib.gzipSync(Buffer.from(s), { level: 9 }).length;
const b64 = 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
const pct = 'data:image/svg+xml,' + encodeURIComponent(svg);
console.log('raw', Buffer.byteLength(svg), 'gzip', gz(svg));
console.log('base64', b64.length, 'gzip', gz(b64));
console.log('percent', pct.length, 'gzip', gz(pct));
出力は下記でした。
raw 3005 gzip 1097
base64 4034 gzip 1787
percent 5566 gzip 1189
パーセントエンコードは文字数だと 5,566 で一番長くなります。
それでも gzip 後は 1,189 バイトで、元の SVG を gzip したものの1.08倍で済みました。
%3C や %22 の繰り返しを gzip が拾えるからです。
ツールでの扱い
今のツールは readAsDataURL の結果をそのまま出しているので、SVG も Base64 になります。
「約33%増えます」の注意書きも、gzip 前の話としては正しいものの、PNG では言いすぎで、SVG では言い足りない、というのが今回わかったことです。
作ったツールはここに置いています。
https://hashitosystem.com/tools/imagebase64/
Data URL の重さは、文字数ではなく gzip 後のバイト数で比べるのが正確です。
PNG や JPEG は気にしすぎなくてよく、SVG を埋め込むときだけは Base64 をやめてパーセントエンコードにしてください。
本記事はAI補助で執筆した、個人開発の紹介記事です。