開発者ツール · Base64 エンコーダーおよびデコーダー
JSON API の Base64: バイナリ フィールドがエンコードされる理由とそのコスト
· なぜそれが重要なのか
base64 エンコード
JSON にはバイト型がないため、バイナリ データは通常、Base64 で文字列にエンコードされます。この投稿では、その規則が存在する理由、サイズと CPU のコスト、および別個のバイナリ エンドポイントがより適切な呼び出しである場合について説明します。
応答を支配する PDF フィールド - 1 つの Base64 BLOB が他のすべてを上回る具体的な API ペイロード
API 応答には、ペイロード サイズの大部分を占める単一フィールドを持つ大きなオブジェクトが含まれています。応答は JSON であるため、すべての値は文字列または数値になります。ユーザー ID、タイムスタンプ、ステータス コードなど、ほとんどのフィールドは小さいです。 1 つのフィールドには imageData または fileContents が含まれており、40 キロバイトの Base64 文字列です。応答全体は 50 キロバイトです。この 1 つのフィールドは転送の 80 パーセントを占めますが、サーバーは最初にバイトとして送信し、クライアントは最終的に再びバイトを必要とするため、無駄に思えます。
Base64 はこの問題を解決しました。JSON にはネイティブのバイト型がないため、バイナリ データを文字列でラップする必要があります。 Base64 は、任意のバイトを JSON で安全な ASCII 文字に変換します。クライアントとサーバーの両方が送信時にエンコードし、受信時にデコードする必要があるため、CPU オーバーヘッドが追加されます。結果として得られるペイロードは、生のバイトよりも約 3 分の 1 大きくなります。この投稿では、その規則が存在する理由、実際のコスト、および別のバイナリ エンドポイントで JSON 制約を破ることに価値がある場合について説明します。
JSON が生のバイトを伝送できない理由 — 文字列は有効な Unicode テキストである必要があるため、任意のバイトにはテキスト ラッパーが必要です
JSON は、すべての値が有効な Unicode テキストである必要があるテキスト形式です。この仕様では、文字列、数値、ブール値、および null が定義されています。バイト配列やバッファ型はありません。 API が画像、暗号化署名、ファイルのアップロードなどのバイナリ データを返す必要がある場合、生のバイトを JSON オブジェクトに直接入れることはできません。バイトには、JSON パーサーが構造マーカーとして解釈する文字が含まれる場合があります。バイナリ BLOB の中央に null バイトがあると、文字列が早期に終了したり、パーサーが壊れたりする可能性があります。
一般的な解決策は、バイナリ データを Base64 としてエンコードし、JSON パーサーがプレーン テキストとして扱う ASCII 文字の文字列を生成することです。次に、受信クライアントは Base64 をデコードしてバイトに戻し、それを使用します。このエンコード手順は API レベルで行われ、ほとんどの開発者には隠されていますが、API が多くのバイナリ フィールドを返すと、実際のコストが累積します。 JSON における Base64 のコストは、要求と応答のサイクル全体にわたって増大します。サイズのペナルティは 1 つ目です。エンコードのオーバーヘッドにより、Base64 出力は入力よりも約 33 パーセント大きくなります。
コスト: 3 分の 1 の追加バイト、デコード時間、メモリ コピー - 各コストは一般的なクライアントで表示されます。
30 メガバイトのビデオ ファイルは、Base64 でエンコードされると 40 メガバイトになります。 30 メガバイトではなく 40 をダウンロードすると、モバイル デバイスの帯域幅とバッテリーが消費され、低速接続ではユーザーに時間がかかります。 2 番目のコストは CPU 時間です。サーバーはバイナリ データを JSON に文字列化する前に、Base64 としてエンコードする必要があります。クライアントは JSON を解析し、各 Base64 フィールドをバイトにデコードする必要があります。複数のバイナリ フィールドを含む応答、または数千の応答を処理するクライアントの場合、CPU 時間は累積します。
電話などの制約のあるデバイスでは、JavaScript 文字列操作と Base64 デコードに使用される TextDecoder がバッテリーを消費し、アプリケーションの速度を低下させます。 3 番目のコストはメモリです。JSON パーサーは Base64 フィールドの文字列オブジェクトを作成し、デコードすると別のコピーが Uint8Array として作成されます。大きなフィールドは、アプリケーションが使用できるようになる前に、メモリ内で 2 回インスタンス化されます。実際の例でコストを明確にします。 API エンドポイントが 100 キロバイトのアバター画像を含むユーザー プロファイル データを返すとします。サーバーはディスクからイメージをバイトとして読み取り、Base64 にエンコードして、JSON 応答に含めます。
実用的な例: API 応答からの Base64 フィールドの検査 — ブラウザでデコードして、サーバーが実際に送信した内容を確認する
JSON 応答は、約 135 キロバイト (33% のオーバーヘッドと他のフィールドを加えたもの) になります。クライアントは 100 ではなく 135 キロバイトをダウンロードします。ブラウザでは、JSON パーサーが Base64 データの JavaScript 文字列オブジェクトを作成します。
アプリケーションで画像が必要になると、Base64 デコーダーが呼び出され、元の 100 キロバイトの Uint8Array が作成されます。数ミリ秒のデコードの間、両方のオブジェクトがメモリ内に存在します。ページにアバター付きのプロフィールが 10 個表示される場合、コストは倍増します。別の方法は、API がアバター リソースごとに個別の URL を含む JSON 応答を返し、ブラウザーがネイティブ キャッシュ、プログレッシブ レンダリング、およびメモリ管理を使用して画像のダウンロードを処理できるようにすることです。
代替案: マルチパート、個別のダウンロード URL と生のバイナリ エンドポイント — それぞれのトレードオフ
バイナリを JSON に含めるか、バイナリを個別に取得するかのトレードオフは、API の目的と使用パターンによって異なります。何百もの小さなサムネイル画像を返す検索結果ページの場合、それぞれを個別のリクエストとして取得すると、HTTP 接続のプールとキャッシュが無効になります。 JSON 応答で Base64 としてインライン化すると、より高速になる可能性があります。 1 つまたは 2 つの高解像度画像を要求する詳細なプロフィール ページの場合は、個別にダウンロードした方が明らかに優れています。 API ドキュメントには、Base64 フィールドの最大サイズと、クライアントが別のエンドポイントを予期する必要がある場合について記載する必要があります。
フィールドが定期的に 1 ~ 2 キロバイトを超える場合は、インライン Base64 戦略は API 設計を再検討する必要があることを示しています。 JSON の Base64 の代替手段は存在しますが、それぞれにトレードオフがあります。マルチパート MIME 応答はバイナリとテキストを分離するため、バイナリ セクションは生のバイトとして送信され、テキスト セクションのみが JSON になります。これには、クライアントが JSON.parse を呼び出すだけではなく、マルチパート メッセージを解析する必要があり、複雑さが増します。 JSON 応答内の別のダウンロード URL は、バイナリ リソースを個別に取得するようにクライアントに指示します。
API ドキュメントに記載する価値のある規則 - 標準と Base64URL のアルファベット、パディング、および最大サイズ
これは、バイナリ リソースが大きい場合、またはメタデータよりもアクセス頻度が低い場合にうまく機能します。バイトのみを返し、JSON を完全に破棄する生のバイナリ エンドポイントは最も単純なアプローチですが、JSON が提供する構造が削除されます。一部の API は圧縮されたバイナリ データを返し、それを Base64 エンコードするため、サイズのペナルティは減りますが、解凍のオーバーヘッドが追加されます。選択は予想される使用法によって異なります。小さなフィールドはインラインで問題なく、大きなフィールドは別のリソースに属し、構造化データは Base64 のコストを考慮しても JSON に保持する価値があります。
規則は相互運用性にとって重要です。バイナリ データを Base64 エンコードする API は、バイナリ データを明確に文書化し、アルファベットが標準であるか URL セーフであるかを示す必要があります。標準 Base64 は + と /, を使用します。これらは JSON 文字列では安全ですが、URL では安全ではありません。 URL セーフな Base64 はそれらを - と _ に置き換えます。これは data: URI に適していますが、JSON では不必要にエスケープされます。ドキュメントでは、パディングを含めるか省略するかを指定する必要があります。どちらも有効な Base64 ですが、パディングを予期し、パディングされていないデータを受信するクライアントは、通知せずに失敗するか、ガベージが生成されるためです。
これでカバーされないもの — protobuf、CBOR、およびその他のバイナリ シリアル化形式
非常に大きいフィールドまたは頻繁に更新されるフィールドの場合、クライアントが何キロバイトもの不必要なデータをフェッチしようとしないように、別個のバイナリ エンドポイントを文書化することが不可欠です。 Base64 フィールドを使用した API のデバッグは、適切なツールを使用すれば簡単です。 Base64 エンコーダおよびデコーダを使用すると、フィールドを保存したり、どこにも送信したりせずに、ブラウザ内でローカルにフィールドをデコードできます。 JSON 応答から Base64 フィールドをコピーし、デコーダーに貼り付けて、[デコード] をクリックします。テキストのようなデータ (たとえば、Base64 内の JSON) の場合、デコードされた出力が即座に表示されます。
画像などのバイナリ データの場合、16 進ビューにはバイトが表示されます。これは、サーバーが期待した内容を送信したこと、およびクライアント デコーダーが正しく動作していることを確認するのに役立ちます。フィールドが予期しないデータにデコードされる場合、問題はサーバーのエンコードまたはフィールドのコピー方法にあります。部分的な BLOB にデコードされる場合は、フィールドが切り詰められているか、Base64 の長さが間違っている可能性があります。ローカル デコードでは、フィールドをファイルに書き込んで外部ツールを開く場合と比較して、デバッグが高速化されます。
要点: JSON の Base64 は妥協なので、それを文書化してください — Base64 エンコーダーとデコーダーがエンコードされたフィールドをローカルで検査および検証するのにどのように役立つのか
API における Base64 への実際的なアプローチは、回避ではなく認識です。 Base64 は JSON でバイナリ データを運ぶ標準的な方法であり、機能します。各 Base64 フィールドのサイズは 3 分の 1 増加し、要求と応答のサイクルごとに数ミリ秒の CPU 時間がかかることを理解してください。認証トークン (JWT 自体が Base64 でエンコードされている場合) のような小規模で重要なメタデータの場合、コストは無視できます。大きな添付ファイルの場合は、バイナリを同じ応答で送信する必要があるのか、それとも別のリソースとして送信する必要があるのかを検討してください。
API 仕様にエンコード スキームと最大サイズを文書化します。応答を検査するときは、Base64 エンコーダとデコーダを使用して、フィールドが正しくデコードされていることを確認し、サーバーが実際に送信した内容を理解します。この規律により、トレードオフが可視化され、意思決定が偶然ではなく意図的に行われます。