Web APIで画像を送信する際、「バイナリ(multipart/form-data)で送るか」「Base64エンコードしてJSONに埋め込むか」で迷うことがあります。
よく言われるのが 「Base64にするとデータサイズが約1.33倍に増えるから、パフォーマンス的に不利である」 という説です。
しかし、実際に検証してみると、 「HTTP圧縮(Gzip/Brotli)を有効にしていれば、Base64の増加分はほぼキャンセルされ、バイナリ転送と変わらないサイズになる」 という結果になります。
なぜ、すでに圧縮済みのJPG画像に対して、さらに圧縮をかけてサイズが縮むのでしょうか?
この記事では、この現象を 情報理論(エントロピーと冗長性) の観点から解説します。
当たり前なのでは?と思えた人はこちらを開いてください
予想外に多くのはてぶを頂いたので追記しました。
https://b.hatena.ne.jp/entry/s/qiita.com/shiozaki/items/9d7aeac0dd6733a6e2fb
この記事もエントロピーが低いから圧縮して三行にしよう。
最高のコメントですね!でも三行も要らないです。タイトルの一行で十分です
Q: 当たり前のことですよね?
A: そうですね、当たり前です
Q: 当たり前のことが何でこんなにいいねされてるんですか?
A: 不思議ですね、僕もよく分からないです
Q: AI使って記事書きましたか?
A: はい
Q: 何でAI使ったんですか?
A: 「当たり前」の一言で説明がつく話に労力をかけたくなかったからです
Q: 一部の記述が間違っていませんか?(ハルシネーション含む)
A: 大変失礼いたしました、具体的な箇所をコメントなどで教えていただければ幸いです
結論
-
通信量(転送サイズ):
HTTP圧縮(Gzip等)が有効な環境下では、Base64送信とバイナリ送信の転送サイズに大きな差はない。Base64の増加分は圧縮によって回収される。 -
CPU・メモリコスト:
サイズは同じでも、Base64方式はエンコード・デコード・圧縮・展開のプロセスが増えるため、CPU負荷とメモリ消費量は高くなる。
謎:なぜJPG画像のBase64文字列がGzipで縮むのか?
一般的に、JPGやPNGなどの画像ファイルは既に高度に圧縮されており、これ以上Gzipをかけてもサイズは減らない(むしろヘッダ分で微増する)と言われています。
しかし、これをBase64エンコードしたテキストデータに対してGzipをかけると、劇的にサイズが減り、元のバイナリサイズとほぼ同じになります。
直感的な疑問
「Base64にすると、ランダムな英数字の羅列になる。ランダムなデータは圧縮が効きにくいのでは?」
答え
Base64の文字列は、人間にはランダムに見えますが、コンピュータ(圧縮アルゴリズム)から見ると「スカスカ(冗長)」なデータだからです。
情報理論による解説
この現象は、エントロピー(平均情報量)とハフマン符号化の仕組みで説明がつきます。
1. 「33%増加」の正体は「冗長性」
元のJPGバイナリデータは、圧縮済みであるためエントロピー(情報の密度)が非常に高い状態です。1バイト(8bit)の中に、ほぼ8bit分の情報が詰まっています。
これをBase64に変換すると、データサイズは4/3倍(約133%)になります。しかし、元の画像が持っている情報の総量は変わりません。
単純に、「情報量が変わらないまま、容器だけが大きくなった」状態です。
-
バイナリ: 0〜255の256種類の値をフルに使う。
-
エントロピー bit/byte
-
Base64: 8bitの器(ASCIIコード)を使っているが、実際に出てくる文字は
A-Z, a-z, 0-9, +, /の64種類しかない。 -
実質的な情報量 = bit
つまり、Base64のデータは 「1バイトにつき2bit分の『無駄(冗長性)』を含んでいる」 ことになります。
この「2bitの隙間」が、ファイルサイズを33%押し上げている犯人です。
2. ハフマン符号が「隙間」を回収する
Gzip(Deflateアルゴリズム)の後半工程では、ハフマン符号化が行われます。これは「出現頻度の高い文字に短いビット列を割り当てる」仕組みです。
Base64データをGzipに入力すると、ハフマン符号器は極端な「文字の出現頻度の偏り」を検知します。
- 使われない約190種類の記号: 出現率 0%
- 使われる64種類の記号: 出現率 100%
情報理論の定理に基づき、64種類の記号しか現れないデータソースは、1文字あたり平均 6 bit で表現可能です。ハフマン符号は、Base64化によって人工的に引き伸ばされた「8bit表現」を、本来の「6bit表現」に再圧縮して詰め直します。
3. 計算上の収支
サイズの変化を計算式で追うと、見事に「行って来い(プラスマイナスゼロ)」になります。
- 元データ(バイナリ)
- データ量: 3 MB
- Base64エンコード (6bitずつに分割して4文字にする)
- 文字
- データ量: 4 MB (ここで33%増加)
- Gzip圧縮 (偏りを利用して1文字を6bitに詰め直す)
- バイナリ
- データ量: 3 MB (ここで元に戻る)
これが、Base64画像を圧縮すると元のバイナリサイズに戻るカラクリです。
画像の「中身」を圧縮しているのではなく、Base64という「形式」が持っている無駄を削ぎ落としているだけなのです。
4. 本当に理論通りの結果になるのか実験
こちらの画像を使って理論通りになるのかを実験してみます。
まず、元データのサイズは234KBです。
cat shikun20220402_125829_TP_V.jpg | wc -c
234186
base64エンコードをすると、データ量が33%増加します。
cat shikun20220402_125829_TP_V.jpg | base64 | wc -c
312249
そして、その後にgzip圧縮をすると、ほぼ元通りの235KBになっていることが分かります。
cat shikun20220402_125829_TP_V.jpg | base64 | gzip | wc -c
235393
結局、どっちを採用すべきか?
「通信量が変わらないなら、JSONで扱いやすいBase64一択でいいのでは?」と思うかもしれませんが、CPUとメモリという隠れたコストが存在します。
Base64 (JSON埋め込み)
メリット:
- リクエストが1つのJSONで完結するため、フロントエンドの実装がシンプル。
- RDBやNoSQLにそのまま文字列として保存する場合などにハンドリングしやすい。
デメリット:
-
CPU負荷:
Bin -> Base64(Client) ->Gzip(Client) ->Gunzip(Server) ->Base64 -> Bin(Server) という変換コストがかかる。 - メモリ効率: Base64文字列の生成やJSONパース時に、バイナリサイズの数倍のメモリを消費することがある。
バイナリ送信 (multipart/form-data)
メリット:
- CPU負荷: エンコード/デコード処理が不要。右から左へ流すだけ。
- メモリ効率: ストリーム処理がしやすく、巨大なファイルでもメモリを圧迫しにくい。
デメリット:
- JSONデータと同時に送る場合、
multipart/form-dataの構築が少し面倒(メタデータはJSON文字列としてパートに含めるなど)。
まとめ
- Base64によるサイズ増加(約33%)は、HTTP圧縮によってほぼ帳消しにできる。
- したがって、帯域幅(通信速度)だけを理由にBase64を避ける必要はない。
- ただし、CPUリソースとメモリ消費の観点ではバイナリ送信(
multipart/form-data)が有利。
「Base64は重い」という常識は、半分正解で半分間違いでした。ネットワーク的には重くないが、計算資源的には重い。この特性を理解してアーキテクチャを選定しましょう。