正確なサイズ計算: Base64 は圧縮で取り返せない 33% の税金
Base64 は入力 3 バイトを出力 4 文字に対応させるため、エンコード後の長さは正確に 4 × ceil(n / 3) で、末尾に最大 2 個の = パディングが付きます。まず何よりも固定の +33.3% が発生します: 30 KB の PNG は約 40 KB のテキストになり、750 KB の写真はまるまる 1 メガバイトになります。アルファベットは A–Z、a–z、0–9、+、/ です(RFC 4648)。+ と / は URL やクエリ文字列では安全でない点に注意してください — base64url 変種がこれらを - と _ に置き換えるのはそのためで、標準アルファベットの文字列を URL パラメータにそのまま貼り付けるのは古典的なデータ破損の原因です。
この税金は見た目より悪質です。転送時の圧縮がほとんど効かないからです。PNG、JPEG、WebP のペイロードはすでに圧縮済みで、バイト列は統計的にほぼランダムです。Base64 を通してもランダムなままで、単に 64 文字のアルファベットに広げられただけです。周囲の HTML にかかる gzip や brotli は、エンコードされた画像から通常 2〜5% しか取り返せません — 通常の HTML/CSS/JavaScript テキストで一般的な 70〜80% の圧縮率と比べてください。つまり、圧縮されたページにインライン化された 100 KB の画像は、ワイヤ上で依然としておよそ 128 KB を占めます。
無視してよい唯一のオーバーヘッドはプレフィックスです: data:image/png;base64, が加えるのは約 22 文字だけ。重要なのは 33% であり、このコストはそのマークアップを含むすべてのページビューで再び支払われます。
const encodedSize = (bytes) => 4 * Math.ceil(bytes / 3);
encodedSize(30 * 1024); // 30 KB PNG -> 40 960 chars (+33.3%)
encodedSize(750 * 1024); // 750 KB JPEG -> ~1 MB of text
// gzip/brotli barely help: compressed image bytes remain
// statistically random after Base64 — only ~2–5% comes backインライン化で失うもの: キャッシュ、遅延読み込み、Preload Scanner
ホストされた画像ファイルは HTML とは独立にキャッシュされます: コピーの一文を変更すれば、再訪者はその一文だけを再ダウンロードし、ロゴは再取得しません。インライン化された画像はドキュメントに融合しているため、すべての HTML レスポンスに同乗し、デプロイのたびに無効化されます。ページをまたぐと損失は倍増します — 10 個のテンプレートに繰り返された data URL は 10 回ダウンロードされますが、長い Cache-Control max-age が付いたホスト済みファイルは 1 回で済みます。
遅延読み込みも静かに機能しなくなります。loading="lazy" は画像のネットワークフェッチを先送りする仕組みですが、インライン画像には先送りすべきフェッチ自体がありません — バイトはドキュメント内に埋まっており、その下にあるすべてのコンテンツより先に転送されるため、事実上、ファーストビュー外(below-the-fold)の画像がレンダリングを妨げるペイロードに変わります。HTML パース中に <img> の URL を発見して並列フェッチするブラウザの preload scanner も失い、CDN 側の画像最適化も失います: フォーマットネゴシエーション(Accept ヘッダーに応じた AVIF/WebP)、リサイズ、品質調整はすべてアドレス可能な URL を必要とします。
インライン化の歴史的な根拠 — HTTP リクエストを 1 つ節約する — は HTTP/1.1 とともに死にました。HTTP/2 と HTTP/3 では、ウォームな接続上のリクエストは多重化され、ラウンドトリップではなく数十バイトのフレームオーバーヘッドしかかかりません。残るのは狭いスイートスポットだけです: ファーストペイントで描画されるおよそ 2〜5 KB 以下のアセット — UI アイコン 1、2 個や、本物の写真が読み込まれる間の場所を確保するぼかし LQIP プレースホルダーなどです。
data: URL の解剖(RFC 2397) — SVG が Base64 を使うべきでない理由
RFC 2397 の文法は data:[mediatype][;base64],<data> です。カンマとペイロード以外はすべて省略可能ですが、デフォルト値こそが罠です: media type を省略するとブラウザは text/plain;charset=US-ASCII とみなすため、完全に有効な Base64 PNG がプレーンテキストとして解釈され、壊れた画像アイコンとして表示されます。常に data:image/png;base64, を完全に書いてください — このツールは自動で付けます。
SVG は特別扱いが必要です。SVG はテキストなので、Base64 は間違ったコンテナです: 33% の膨張コストを払ううえ、gzip がページ内の繰り返し XML パターンを見つける能力を破壊します。より良いエンコードはプレーンな URL エンコード data URL です: data:image/svg+xml,%3Csvg ... URL や CSS のパースを壊す文字だけをパーセントエンコードすれば十分です — <、>、#、引用符。文字と大半の記号はそのままで構いません。結果は通常、圧縮前で Base64 版より 15〜30% 小さく、SVG ソースが圧縮可能なテキストのまま残るため圧縮後は劇的に小さくなります。# のルールは二重に重要です: fill="#f60" には生の # が含まれており、エンコードしないと URL がフラグメントとして打ち切られます — %23 にエンコードしてください。
文字セットの罠がもう一つ: SVG に非 ASCII テキスト(日本語ラベル、矢印、絵文字)が含まれる場合、素朴な URL エンコード data URL では文字化けすることがあります。該当バイトを UTF-8 としてパーセントエンコードするか、そのファイルだけ Base64 にフォールバックしてください。
/* Raster: Base64 is the only option */
.logo {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANS...");
}
/* SVG: URL-encode instead — smaller and gzip-friendly */
.icon {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 2 22h20z' fill='%23f60'/%3E%3C/svg%3E");
}
/* Missing media type = text/plain assumed = broken image */
/* BAD: data:;base64,iVBORw0KGgo... */data: URL が静かに失敗する場所: CSP、Gmail、ナビゲーションブロック
Content Security Policy が最初の待ち伏せです。data: スキームは 'self' に含まれないため、img-src 'self' のようなポリシーはページ上のすべてのインライン画像をブロックします — 壊れた画像アイコンすら出ず、空白だけが残り、コンソールの奥に違反ログが埋もれることも珍しくありません。画像をインライン化するならポリシーに img-src 'self' data: を明示的に入れる必要があり、セキュリティレビュアーは当然理由を尋ねるでしょう — script-src や object-src の data: は本物の XSS ベクターだからです。画像にだけ許可してください。
メールが 2 番目です。Gmail は <img src> の data: URI を丸ごと除去し、他の大半のクライアント(デスクトップ版 Outlook、Yahoo)も同様に振る舞います — 「外部リクエストがない」という点がメール向きに聞こえるだけに、人々の期待とは正反対です。HTML メールのアイコンは HTTPS URL でホストするか、CID 参照のインライン添付にする必要があります。実際のクライアントでテストしてください。ESP のプレビューは真実ではありません。
ブラウザはナビゲーションも強化しました: 2017 年以降(Chrome 60、Firefox 59)、data: URL へのトップレベルナビゲーションはブロックされます。フルページの data: ドキュメントがフィッシングのラッパーとして人気だったためです。<img>、CSS、fetch() 内でのレンダリングは今も問題なく動きます。長さについて: 現代の Chrome と Firefox は src 属性やスタイルシート内の数メガバイトの data URL を文句なく処理しますが、2 MB を超える URL は devtools でのコピー・diff・検査が苦行になります — そしてレガシー IE8 は URL 全体を 32 KB に制限しており、古いアドバイスが今もその数字を引用するのはこのためです。
メモリコスト、atob() の Unicode の罠、そしてバンドラーに任せる判断
インライン化された画像は RAM で 3 回代金を払います。ブラウザはドキュメント内の Base64 文字列、デコードされたファイルバイト、そして — 画像が表示された瞬間から — 幅 × 高さ × 4 バイトのデコード済みビットマップをすべて保持します。最後の項が他を圧倒し、ファイルサイズとは完全に無関係です: 1000 × 1000 の PNG は、ファイルが 20 KB でも 2 MB でも、ビットマップとしては約 4 MB を占めます。JavaScript バンドルに焼き込まれた Base64 は最悪の変種で、文字列がさらに JS のパースを通り、ヒープに常駐するからです。
このツールではなくコードでエンコードする場合は、古典的な罠に注意してください: atob() と btoa() は Latin-1 文字列でしか動作しません。btoa('日本語') は InvalidCharacterError を投げ、unescape(encodeURIComponent(...)) に基づく Stack Overflow の回避策はすべて deprecated です。ファイルには FileReader.readAsDataURL(このページが使う方式)が正解で、この問題に一切触れません。バイト配列には、最近標準化された Uint8Array.prototype.toBase64() と Uint8Array.fromBase64() がようやくバイナリを正しく扱います。
判断ルールは 3 条件に凝縮されます: ファイルが約 5 KB 未満で、ファーストペイントで描画され、複数ページで再利用されない場合にのみインライン化する。それ以外はすべてホストされたファイルのままにすべきです。そしてバンドラーがすでにこのルールを実装している点に注目してください: Vite はデフォルトで 4 KB 未満の import されたアセットをインライン化し(build.assetsInlineLimit: 4096)、webpack 5 の asset modules は parser.dataUrlCondition.maxSize 経由でデフォルト 8 KB です。ビルドツールがファイルとして出力すると決めた画像を手でインライン化しているなら、たいていツールの方が正しい。
// vite.config.js — Vite already makes the inline/file call
export default {
build: {
assetsInlineLimit: 4096 // bytes; imports under 4 KB become data: URLs
}
};
// webpack 5 equivalent (asset modules)
module.exports = {
module: {
rules: [{
test: /\.png$/i,
type: 'asset', // data URL under maxSize, separate file above
parser: { dataUrlCondition: { maxSize: 8 * 1024 } }
}]
}
};