Base64 形式を扱う必要がありますか?それならこのサイトが最適です!データをエンコードまたはデコードするために便利なオンラインツールをご利用ください。

JavaScript/Node.js での Base64 エンコード:完全ガイド

テキスト化すべきデータがあるとしましょう。JSON フィールドの中に乗り込まなければならないファイル、CSS ファイルの中に住みたい画像、環境変数に座るシークレット、クエリ文字列を旅するトークン。JavaScript と Node.js での答えは、ほぼいつも同じです:Base64。この記事は梱包マニュアルです。あなたが最初のバイトを手にしている瞬間から、エンコードされた文字列がマシンを離れる瞬間までを扱います。

このサイトのホームページでは、アルファベット、計算、パディングまで、形式を詳しく説明しているので、ここでは 1 文にします:3 バイトが 4 個の表示可能文字になる、ということです。だから出力は入力より約 33% 大きくなります。覚えておいてください。この記事のすべてのセクションが存在する理由であり、ストレージの請求書が計算される単位でもあるからです。

安心材料です:インストールするのは何もありません。すべてのモダンなブラウザは btoa() と、より新しい Uint8Array.toBase64() を搭載しており、実用上すべての Node.js バージョンは Buffer クラスを持ち、'base64' モードを、さらにバージョン 15.7.0 からは一級エンコーディングの 'base64url' モードも持っています。腕の見せ所は、自分がどの形の入力を手にしているか、目的地がどのアルファベットを要求するか、古い形式がまだ強制している行折り返し規則が何かを知ることです。

エンコードする前に、入力を知らなければならない

すべてのエンコードの問いは、同じ 1 つから始まります:今、あなたの手元にあるのは何ですか。JavaScript の文字列は UTF-16 のテキストで、Buffer はバイト配列であり、正しい呼び方はどちらを持っているかに依存します:

手元にあるもの 呼ぶもの 備考
ASCII のみの文字列(256 未満の文字) btoa(string) ブラウザと Node.js 16 以降では最速の経路ですが、1 バイトに収まらない最初の文字で止まります
どんな Unicode 文字列でも TextEncoder でバイトに変え、そのあと base64 呼び出し UTF-8 の橋;アクセント付き文字と絵文字への唯一の安全な経路
Buffer または Uint8Array buffer.toString('base64') または bytes.toBase64() Node.js の主力馬、そしてモダンなブラウザと Node.js 25 以降の ES2026 メソッド

3 つの例、表の各行につき 1 つずつ:

// ASCII のみテキスト:レガシーな近道(ブラウザと Node.js 16 以降)
console.log(btoa('hello world')); // "aGVsbG8gd29ybGQ="
// Node.js でのどんなテキスト:Buffer はデフォルトで UTF-8 を読む
const { Buffer } = require('node:buffer');
console.log(Buffer.from('héllo ⛳', 'utf8').toString('base64')); // "aMOpbGxvIOKbsw=="
// すでに自分が持つバイト
console.log(Buffer.from([1, 2, 3, 4]).toString('base64')); // "AQIDBA=="
console.log(new Uint8Array([1, 2, 3, 4]).toBase64()); // "AQIDBA=="(ES2026 ランタイム)

2 つ目の例に注意してください:同じテキストでも、エンコードする文字セットによって異なる Base64 文字列になります。それはバグではなく - このゲーム全体です。Base64 層がエンコードするのはバイトであり、文字列がバイトになるのは文字セットを選んだときだけなので、「このテキストをエンコードする」と言うのは、いつもこっそり「このテキストの UTF-8 バイトをエンコードする」ことを意味しています(そう言ってくれるなら Latin-1 バイトでも)。

Unicode の壁、そしてその上の橋

btoa() はこの場にいる中で最も古い API で、その契約は 1990 年代ものです:入力文字列の各文字は 1 バイトに収まり、コードポイントは 0 から 255 でなければならない。それ以上のもの、絵文字、アクセント付きのキリル文字、漢字は、例外を投げます:

try {
  btoa('héllo ⛳');
} catch (error) {
  console.log(error.name); // "InvalidCharacterError"
  console.log(error.message); // "Invalid character" (Node);ブラウザでは Latin1 レンジに関する言い回し
}

処方箋は、文字で考えるのをやめて、バイトで考え始めたことにあります。TextEncoder(すべてのブラウザと Node.js のグローバル)が文字列を UTF-8 バイト列に変え、あなたはそれらのバイトを Latin-1 文字列に持ち上げると、btoa() は自分が処理すると約束したものこそを手に入れます:

function encodeUnicode (text) {
  const bytes = new TextEncoder().encode(text);
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  return btoa(binary);
}
console.log(encodeUnicode('héllo ⛳')); // "aMOpbGxvIOKbsw=="
console.log(encodeUnicode('héllo ⛳') === Buffer.from('héllo ⛳', 'utf8').toString('base64')); // true、同じバイト

コードベースでは古いイディオムにも出会うでしょう。裏側の仕組みは同じです:btoa(unescape(encodeURIComponent(text)))。encodeURIComponent の呼び出しがパーセントエンコードされた UTF-8 バイトを生成し、unescape がパーセントエスケープを生文字列に戻します。escape も unescape もレガシー関数なので、新しいコードは TextEncoder の橋を優先すべきですが、古い形を受け継いだとき、あなたはもう肩をすくめる代わりに、それがまさに何をしているのかを知っています。

Node.js ではこの壁はほぼ非事です。Buffer.from(text) は UTF-8 を前提とし、同じ呼び出しの中でバイト変換を代わりにやってくれるから。橋が最も重要なのはブラウザです。そこでは btoa() がレガシーな選択肢であり、UTF-8 ステップを明示するのはあなたの仕事だからです。

バイトが入り、文字が出る:Buffer、パディング、フレーバー

バイトを手に入れたら、Node.js のエンコード側は 1 つのメソッドで済みます:toString('base64')。グループの計算も、パディングも、すべてを処理し、RFC 4648 の意味での正規出力、つまり最後のグループの未使用パッドビットがゼロである出力を、常に生成します:

const { Buffer } = require('node:buffer');
const fox = Buffer.from('The quick brown fox jumps over the lazy dog');
console.log(fox.length); // 43 バイト
console.log(fox.toString('base64')); // "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=="
console.log(fox.toString('base64').length); // 60 文字、33% の課税が実際に働いている

末尾のパディングは本物の仕事をしています。装飾ではありません。残り 1 バイトの最終グループは Base64 文字 2 個プラス = 2 個になり、残り 2 バイトのグループは文字 3 個プラス = 1 個になります。出力がそのパディングを伴ってよいかは目的地次第で、それが毎日使う 2 つの Base64 フレーバーの違いです:

const one = new Uint8Array([72]);
console.log(one.toBase64()); // "SA=="(ES2026、パディング込み)
console.log(one.toBase64({ omitPadding: true })); // "SA"
console.log(Buffer.from([72]).toString('base64url')); // "SA"、Node は base64url モードでパディングを落とす

サイズを決めるときには比率を覚えておいてください:3 バイトが入り 4 文字が出るので、データ 1 MB はテキストにして約 1.33 MB になります。さらにメールや PEM のためにテキストを行に折り返すと、改行がその上に数パーセント上乗せされます。

バイトを配線に乗せる

JavaScript で最も一般的な配線問題は、JSON にバイトがないことです。あるのは文字列で、どんな JSON パーサ、どんな HTTP プロキシ、どんなログ記録システムでも安全に旅できる文字列は Base64 のものです。このパターンは接続の両端で同じです:境界でエンコードし、境界でデコードし、その間でバイトを保持する:

const fs = require('node:fs');
const photo = fs.readFileSync('./photo.jpg', 'base64');
const payload = JSON.stringify({
  name: 'photo.jpg',
  contentType: 'image/jpeg',
  data: photo
});
console.log(payload.startsWith('{"name":"photo.jpg"')); // true、ファイルはもう普通の JSON の中に乗っている

このパターンに戦うべき時を知っておくこと。あなたのトランスポートがすでにバイナリをサポートするなら、それを使いましょう:multipart/form-data アップロードはサイズ課税なしに生のファイルを送り、WebSocket フレームは生バイトを運び、Postgres の bytea 列はネイティブで保存します。生バイトが許されていた場所での Base64 は純粋なオーバーヘッドであり、何も残さない 33% の課税です。Base64 がその価値を正当化するのは、チャネルがテキストのみのときです:JSON API、メールの本文、環境変数、URL クエリ文字列、そしてテキストのみを通す多くの橋(モバイル SDK、デスクトップアプリ、チャットシステム)です。

Data URL:テキストの中に生きる絵

data URL、すなわち data:image/png;base64,... という文字列は、MIME ラベルを被った Base64 であり、これが 1 つの HTML 属性の中に画像全体を入れられる理由です。1998 年にこのスキームを定義した RFC は、初期の HTML は属性値に 1024 文字の制限があったため、それが「短い値でのみ有用である」とまで言っています。モダンなブラウザはその制限を笑い、メガバイトサイズの data URL を快くレンダリングします。それはスーパーパワーであると同時に、罠でもあります。

ブラウザでは canvas API がすべての仕事を代わりにやってくれます。ピクセルが入り、data URL が出る:

const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const context = canvas.getContext('2d');
context.fillStyle = '#ff0000';
context.fillRect(0, 0, 1, 1);
const dataUrl = canvas.toDataURL('image/png'); // "data:image/png;base64,iVBOR..."
console.log(dataUrl.slice(0, 24)); // "data:image/png;base64,iV"

そして Node.js を含むどんなランタイムでも、1 つを組み立てるのは、メタデータを正しい場所に置いた文字列連結に過ぎません:data: プレフィックス、メディアタイプ、任意の ;base64 マーカー、カンマ、そしてペイロード。;base64 マーカーがないと、ペイロードは代わりにパーセントエンコードされたテキストであることが期待され、それがマーカーが存在する理由です:

const { Buffer } = require('node:buffer');
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB', 'base64');
const dataUrl = 'data:image/png;base64,' + png.toString('base64');
console.log(dataUrl.startsWith('data:image/png;base64,')); // true

正直なトレードオフです:data URL はドキュメントの一部なので、独自の資源としてキャッシュできず、それが置かれている HTML や CSS のサイズにカウントされ、DOM がそれを解析して保持しなければなりません。20 キロバイトのアイコンなら、それは買いです。4 メガバイトのヒーロー画像なら、キャッシュと圧縮がどちらも効く HTTP でファイルを届け、data URL は小さいものに使いましょう。

文字列として旅するファイル

ファイルから Base64 への変換は、本来 2 ステップの踊りですが、両方のランタイムはそれを 1 回の呼び出しに圧縮します。Node.js ではファイルシステムが読み取りエンコーディングとして 'base64' を受け入れ、書き込み側も受け入れます:

const fs = require('node:fs');
const { Buffer } = require('node:buffer');
const base64 = fs.readFileSync('./report.pdf', 'base64');
console.log(base64.length); // ファイル、約 33% 重くなっている
fs.writeFileSync('./report.pdf.b64', base64, 'utf8');
const copy = Buffer.from(base64, 'base64');
fs.writeFileSync('./report.copy.pdf', copy);

ブラウザでは FileReader が同じ仕事をして、1 つのひねりがあります:データ読み取りモードは data URL を手渡すので、プレフィックスを切り落とすことで素の Base64 ペイロードを手に入れます:

const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', () => {
  const reader = new FileReader();
  reader.onload = () => {
    const dataUrl = reader.result; // "data:application/pdf;base64,..."
    const payload = {
      name: fileInput.files[0].name,
      data: dataUrl.slice(dataUrl.indexOf(',') + 1)
    };
    console.log(payload.data.length); // ファイル、JSON リクエストの準備完了
  };
  reader.readAsDataURL(fileInput.files[0]);
});

ブラウザで data URL プレフィックスなしの生 Base64 が必要なら、file.arrayBuffer() のあと Uint8Array.toBase64()(それを備えたランタイムでは)とすれば、プレフィックスを丸ごとスキップでき、アップロードのパイプラインにはよりきれいな経路です。

base64url: URL を生き延びるアルファベット

クラシックな Base64 は、URL がアレルギーを持つ 2 つの文字を携えています。クエリ文字列がフォームデコードされるたびに + は空白になり、/ はパスセパレータであり、= パディングは代入のように見えます。RFC 4648 セクション 5 の URL とファイル名安全バリアント、base64url は、この 2 つの特殊文字を - と _ に交換し、長さが文脈からわかるときはパディングを落とします。それは JWT、OAuth トークン、ディープリンクのアルファベットであり、あなたの心のツールキットに専用の場所を値します。

Node の Buffer はバージョン 15.7.0 からこの方言を話し、エンコード側は 1 つの引数で済みます:

const { Buffer } = require('node:buffer');
const classic = 'k+XS/B4=';
console.log(Buffer.from(classic, 'base64').toString('base64url')); // "k-XS_B4"

注目すべき点 2 つ。+ が - になり、/ が _ になり、パディングは消えました。base64url モードは設計上それを省略するからです。そして IETF は、これが仮面を被った同じエンコーディングではなく別のエンコーディングであると明言しています。だから仕様が「base64url」と言うとき、検索置換を施したクラシックな Base64 ではなく、base64url を生成すべきです。ES2026 のメソッドは同じ選択を明示的なオプションにし、その omitPadding フラグは、文脈が要求するときにパディングを返してくれます:

console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url', omitPadding: true })); // "q_8"
console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url' })); // "q_8="、2 バイトには 1 つのパッド文字が必要

どちらも持たないランタイムでは、変換は 2 文字の交換とパディングの切り詰めであり、それは JavaScript の世界で最もコピペされるスニペットのひとつです:

const toUrlSafe = (value) => value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
console.log(toUrlSafe('k+XS/B4=')); // "k-XS_B4"

URL、クエリ文字列、ファイル名、トークン標準の中に生きるものは、base64url を使ってください。MIME の本文、data URL、パーセントデコーダーに永遠に会わないものは、クラシックな Base64 を使ってください。この 2 つの取り違えが、この形式全体で最も一般的な互換性バグです。

資格情報を封じる:Basic 認証、JWT、PKCE

ウェブの認証の 3 つのコーナーは Base64 の上に建てられており、3 つとも一度だけなら手作業で作るのにコストはかかりません。これは良いことです。ライブラリの下で何が起きているかを知っていることが、ライブラリに驚かされたときあなたの平静を保つからです。

第一に、HTTP Basic 認証(RFC 7617):クライアントはスキーム語 Basic と user-id:password の Base64 を送ります。1 行、そして 1 つの真剣な警告付き:

const { Buffer } = require('node:buffer');
console.log('Basic ' + Buffer.from('octo:cat').toString('base64')); // "Basic b2N0bzpjYXQ="

ここでの Base64 は隠蔽であって、セキュリティではありません。ヘッダーを読める人は誰でもパスワードを読めるので、このスキームは HTTPS でのみ許容され、それでもなおレガシーなパターンです:トークンを優先してください。第二に、JWT:ドットで区切られた最初の 2 つの部分は素の JSON の base64url で、第 3 が署名です。HMAC-SHA256 トークンを手作業で作るのは、組み込みの crypto モジュールの手数行です:

const crypto = require('node:crypto');
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const payload = Buffer.from(JSON.stringify({ sub: 'octocat', exp: 1893456000 })).toString('base64url');
const signature = crypto.createHmac('sha256', 'topsecret').update(header + '.' + payload).digest('base64url');
const token = header + '.' + payload + '.' + signature;
console.log(token.split('.').length); // 3 つの部分、すべてにパディングなしの base64url

トークンを成功・失敗に分ける詳細に注意してください:どこにもパディングがない(RFC 7515 はそれを省略)、署名は解析済みオブジェクトではなく文字列リテラル header + '.' + payload に対して計算され、全体は鍵が秘密である限りの秘密に過ぎません。本番ではライブラリを使います:jose(依存ゼロ、ブラウザと Node.js)か jsonwebtoken(Node.js)。でもそれらは、裏側でこれと同じ呼び出しを実行しています。第三に、PKCE(RFC 7636): SPA やモバイルアプリのようなパブリッククライアントを安全にログインさせる拡張機能。クライアントは高エントロピーの code_verifier を生成し、BASE64URL(SHA256(verifier)) をチャレンジとして公開し、トークン交換の際にその verifier の所持を証明します。乱数性は重要です。だから verifier は crypto モジュールから来て、決して Math.random() からは来ません:

const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
console.log(verifier.length, challenge.length); // 43 43、どちらも許容される 43-128 の範囲内

古い郵便にはラッピングが必要:MIME と PEM アーマー

世界で最も古い Base64 フォーマットの 2 つは、今もなお行の長さを強制しており、どちらも約 30 年もの昔ものです。RFC 2045 のメール標準である MIME は、その Base64 を 1 行 76 文字で折り返し、行は CRLF で終わることを要求します。これは 8 ビットクリーンな SMTP の時代の名残で、その頃の非常に長い行は本物のメールサーバーを壊していました。証明書と鍵の PEM ルールを書き留めた RFC 7468 は、さらに厳格です:生成側は 1 行ちょうど 64 文字で折り返さなければならず、最終行は短く、内容を名乗る -----BEGIN と -----END のアーマー行に挟まれます。

ラッピング自体は 1 行で済み、アーマーはテンプレートです:

const wrap = (base64, width) => base64.match(new RegExp('.{1,' + width + '}', 'g')).join('\r\n');
const certBase64 = Buffer.from('x'.repeat(150), 'utf8').toString('base64'); // 200 文字
console.log(wrap(certBase64, 76).split('\r\n').map((line) => line.length).join(', ')); // "76, 76, 48"
console.log(wrap(certBase64, 64).split('\r\n').map((line) => line.length).join(', ')); // "64, 64, 64, 8"
const armor = (label, body) => '-----BEGIN ' + label + '-----\r\n' + wrap(body, 64) + '\r\n-----END ' + label + '-----\r\n';
console.log(armor('CERTIFICATE', 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo='));
// -----BEGIN CERTIFICATE-----
// QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=
// -----END CERTIFICATE-----

実践的な注意が 2 つあります。MIME や PEM を生成するときは、ラップしてください。厳格なコンシューマ(メールゲートウェイ、OpenSSL 時代の手前ツール、Java のキーストア)は 4000 文字の 1 行 Base64 blob を拒否するからです。消費するときは通常不要です。Node のデコーダーは空白をスキップするので、Buffer.from が改行を代わりに処理してくれるためです - ただしアーマー行そのものは Base64 ではありません。だからデコード前に -----BEGIN/-----END 行を剥がす(または本文のみをマッチする)必要があります:Buffer.from(pem.replace(/-----[A-Z ]+-----/g, ''), 'base64')。この非対称性は贈り物ですが、Base64 が何もスキップしない場所、たとえば DER パーサーに行くからといって、アーマー剥がしのステップを省いていいという意味ではありません。

エンコードされたデータが住む場所:環境変数、設定、データベース

Base64 は同時にストレージ形式でもあり、それは便利な反面、セキュリティと危険なほど見間違いやすいものです。環境変数は定番の家です:いくつかのシークレットマネージャーや CI システムが Base64 エンコードされた値を手渡し、デコードは 1 行で済みます:

const { Buffer } = require('node:buffer');
const stored = process.env.API_KEY_B64; // "c3VwZXItc2VjcmV0"
console.log(Buffer.from(stored, 'base64').toString('utf8')); // "super-secret"

重要な文を声に出して言ってみましょう:エンコードは暗号化ではありません。環境変数、.env ファイル、Kubernetes シークレット(k8s はシークレットを API でも etcd でも Base64 で保存し、ドキュメントはそれを絶えず繰り返している)にある Base64 の「シークレット」は、プロセスの環境、ファイル、クラスタを読める誰にも読めます。そこで Base64 を使うのは、トランスポート(シェル、YAML、JSON)がテキストのみだからであり、決してそれが何かを隠してくれると信じて使うのではありません。

データベースでは、Base64 が JSON ドキュメントストア内のバイナリの標準的な橋です。jsonb 列も MongoDB のドキュメントも、自分のバイト型を持っていないからです:

const document = {
  name: 'logo',
  mime: 'image/png',
  data: Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64')
};
console.log(JSON.stringify(document)); // {"name":"logo","mime":"image/png","data":"iVBORw=="}

例のように、ペイロードの隣にメディアタイプを保存しておけば、1 年後に誰かが「このバイトは何ですか」と尋ねたとき、今の自分に感謝することになります。データベースにネイティブのバイナリ型があるなら(Postgres の bytea が参考例です)、それを優先しましょう:バイトは追加コストがかからず、33% の課税を永遠に回避できます。

グループを割らずにストリームをエンコードする

Base64 は 3 バイトグループで動作するため、どんなチャンクでも受け取るエンコーダーは剰余を繰り越さなければなりません。まだグループを形成できない 1 バイトまたは 2 バイトは、エンコードされる前に次のチャンクを待つ必要があります。チャンクごとに計算をして、完全なグループだけを出せば、出力はストリーム全体を一度にエンコードした場合とバイト単位で一致します:

const { Transform } = require('node:stream');
const { Buffer } = require('node:buffer');
function base64Encoder () {
  let pending = Buffer.alloc(0);
  return new Transform({
    transform (chunk, _encoding, done) {
      pending = Buffer.concat([pending, chunk]);
      const whole = Math.floor(pending.length / 3) * 3;
      this.push(pending.subarray(0, whole).toString('base64'));
      pending = pending.subarray(whole);
      done();
    },
    flush (done) {
      if (pending.length > 0) {
        this.push(pending.toString('base64'));
      }
      done();
    }
  });
}
let output = '';
const encoder = base64Encoder();
encoder.on('data', (part) => { output += part; });
encoder.on('end', () => {
  console.log(output); // "aGVsbG8gd29ybGQsIHRoaXMgaXMgYSBzdHJlYW0h"、巨大な 1 回の toString('base64') と同一
});
encoder.end(Buffer.from('hello world, this is a stream!'));

flush コールバックは、誰もが忘れる詳細です:最後の 1 バイトまたは 2 バイト - 通常のチャンクでパートナーを見つけられなかったもの - はパディングを与えられ、最後にプッシュされます。同じ繰り越しロジックを、デコード側でも模倣することになりますが、そこでは ES2026 API がそれを無料でくれます:"stop-before-partial" 付きの setFromBase64() はグループ境界でちょうど止まり、消費した文字数を教えてくれます。

大きなファイルとメモリの請求書

Base64 はスペースに対して寛大なので、大きなファイルには戦略が必要です。1 GB のファイルは Base64 テキストとして約 1.33 GB になり、JavaScript の文字列は UTF-16 で保存され 1 文字あたりヒープ 2 バイトなので、Buffer が到着する前に、テキストだけで約 2.7 GB のメモリを要求します。Node では上限が明示されています:buffer.constants.MAX_STRING_LENGTH は 536870888 文字で、テキストにして 512 MiB ほど、デコードすると約 400 MB のバイトです。それを越えると、1 つの文字列という選択肢はなくなり、ストリーミングだけが町のゲームです:

const fs = require('node:fs');
const { Buffer } = require('node:buffer');
let carried = Buffer.alloc(0);
const source = fs.createReadStream('./video.mp4', { highWaterMark: 64 * 1024 });
source.on('data', (chunk) => {
  const joined = Buffer.concat([carried, chunk]);
  const whole = Math.floor(joined.length / 3) * 3;
  process.stdout.write(joined.subarray(0, whole).toString('base64'));
  carried = joined.subarray(whole);
});
source.on('end', () => {
  if (carried.length > 0) {
    process.stdout.write(carried.toString('base64'));
  }
  process.stdout.write('\n');
});

このパターンは、前のセクションのストリームエンコーダーを平らに広げたものです:64 KiB チャンクで読み、1 から 2 バイトの剰余を繰り越し、完全なグループを出し、末尾をフラッシュする。ファイルの重さに関係なく、メモリフットプリントは 1 チャンク加え 1 剰余くらいに留まります。そして受信側がバイナリを受け入れられるなら、なぜあなたがその課税を払っているのか自分に問いましょう。

ターミナル用のワンライナー

Node はコマンドラインの Base64 エンコーダーとしても使えます。設定値のパッケージング、API のデバッグ、チャットメッセージで小さなファイルをマシン間でやり取りするときに便利です:

# ファイルをクラシックな Base64 にエンコードして stdout へ
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64"))' notes.txt
# URL 安全バリアント、パディングは落とされる
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64url"))' notes.txt
# stdin から読む、パイプのためのもの
echo -n "hello world" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>process.stdout.write(Buffer.from(d,"utf8").toString("base64")))'

3 つのうちいずれも、自分の末尾改行は追加しません。これにより、コピー&ペーストにもシェルスクリプトの $(...) 置換にも、出力はきれいなまま保たれます。行が折り返された整形されたファイルが欲しいなら、結果を愛用のエディタへパイプするか、ワンライナーの末尾に \n を追加してください。

開発者に時間を奪った落とし穴

これらすべてが、実在のコードベースで実在の午後の原因になってきました:

  • Unicode の壁:ゴルフの旗が 1 バイトに収まらないため、btoa('héllo ⛳') は InvalidCharacterError を投げます。処方箋は UTF-8 の橋です:まず TextEncoder でバイトに変え、それからエンコード。Node.js では UTF-8 を前提とする Buffer.from(text) で問題を丸ごとスキップできます。
  • レガシーなイディオム:btoa(unescape(encodeURIComponent(x))) で溢れる古いコードは動きますが、escape と unescape は非推奨のレガシー関数です。そのコードをリファクタリングするときは、TextEncoder の橋に置き換えてください。動作は同じのままです。
  • 足りないエンコーディング引数、逆方向版:デコード側の落とし穴は 'base64' モードなしの Buffer.from(str) であり、エンコード側の双子は、Buffer.from(someString) が Base64 で何か特別ことをすると想定することです。しません。明示的なエンコーディングがなければ、文字列の UTF-8 バイトから Buffer を組み立てるだけなので、「エンコードした」出力は文字列の文字バイトの Base64 になり、それが望んでいたものと一致することはほとんどありません。両方向で明示的にしてください。
  • パディングの不一致:JWT とほとんどのトークン標準はパディングなしの base64url を望み、MIME はパディング付きのクラシックな Base64 を望み、この 2 つは簡単に取り違えます。JWT の部分にあるパディング付きの = は strict な検証者を壊し、長さが不明な場所でのパディング不足は寛容なデコーダーを壊します。習慣ではなく標準に合わせてください。
  • 非正規な末尾:RFC 4648 は、最後のグループの未使用パッドビットがゼロであることを要求します。組み込みのエンコーダーはすべて正規の出力を生成しますが、ビットを手作業でシフトする自作エンコーダーは、それらのビットにゴミを残すことがあります。すると strict なデコーダーは、目に見える理由なくあなたのペイロードを拒否します。自分でエンコーダーを書くなら、自分のデータだけでなく、RFC 4648 のテストベクトルに対してテストしてください。
  • 忘れたラップ:MIME は 76 文字の行を、PEM は 64 文字の行を望み、厳格なコンシューマ(メールゲートウェイ、Java の鍵ツール)は 1 行の blob を拒否します。逆はめずらしいものの実在します:いくつかのパーサーは行単位で動くため、PEM ファイル末尾の CRLF 不足が、Base64 自体のどんなバグよりも多くのビルドを壊してきました。
  • セキュリティの錯覚:環境変数、.env ファイル、Kubernetes シークレットにある Base64 は暗号化ではありません。ファイルやクラスタを読める誰にも、どんな言語でも 1 行のコードでデコードできます。それをトランスポート用の仮面として扱い、本物の防御(権限、TLS、鍵のローテーション)に保護を続けさせてください。
  • JSON の膨張:JSON 内の Base64 は 33% にエスケープを上乗せし、5 MB のアップロードは、JSON パーサーがメモリにコピーしなければならない 6.7 MB の文字列になります。HTTP でファイルサイズのものは、multipart/form-data か生のバイナリ本体のほうが良いトランスポートであり、Base64 はチャネルがテキストのみのためにあります。
  • ヒープの請求書:エンコードされた文字列は JavaScript ヒープ上で UTF-16、1 文字 2 バイトであり、デコードされたソースの Buffer はデータの 2 番目のコピーです。100 MB のファイルは一時的に、約 270 MB の文字列と 100 MB の Buffer を意味します。大きいものはストリームにしてください。エンコード済み形が参照され続ける時間は、コードが許す限り短く保ってください。
  • Node のレガシーなグローバル:Node 自身のドキュメントは btoa() と atob() を Stability 3, Legacy とマークし、代わりに Buffer を使うように言っています。ブラウザでは btoa() は ASCII テキストのための完全に問題のない道具ですが、Node.js では Buffer を手に取り、それらのグローバルはポリフィル姿のコードに任せましょう。

JavaScript がバイトを梱包する方法を学んだ道

ブラウザ側の話は、長く、静かなものです。btoa() は 2011 年初頭の HTML5 草案で仕様化されました。2000 年代半ば以降、すべての主要ブラウザに据え置かれ、動作は不変です。1 文字 1 バイトの契約、そして常にパディング付きの出力という形で。その契約は型付き配列より古く - 2009 年以前、バイトを運ぶ唯一の方法はバイナリ文字列でした - だから btoa() は今も「バイナリ文字列」で考えています。この話のモダンな半分は非常に新しく、型付き配列にネイティブな Base64(hex と一緒に)を加えた TC39 の提案が ES2026 の一部として標準化され、2024 年に Firefox 133 と Safari 18.2 に入り、2025 年 9 月 2 日に Chrome 140 に入り、その後 Baseline Newly available と宣言されました。Bun は 2024 年 8 月のバージョン 1.1.22 で同じメソッドを搭載しています。

Node.js は別の時計でバイトを梱包してきました。Buffer クラスは 2010 年夏、バージョン 0.1.103 でグローバルになり、Node 1.0 のほぼ 5 年ほど前でした。そして 10 年以上にわたり toString('base64') が選ばれるエンコーダーでした。あの時代のアルファベットのクセつき(デコード時にはすでに URL 安全文字を受け入れており、仕様が要求したこともない二言語の習慣)です。2021 年 1 月のバージョン 15.7.0 が 'base64url' モードを一級エンコーディング名として追加し、同じ年の Node 16 がブラウザの btoa()/atob() グローバルを追加しました(すぐさま Legacy とマーク)。2024 年の Node 22 がさらに V8 と base64 のパフォーマンス作業を搭載しました。その後、2025 年 10 月 15 日にリリースされた Node 25 が V8 を 14.1 に引き上げ、ES2026 のメソッド - omitPadding オプション付きの toBase64() と、逆方向の setFromBase64() - をランタイムにもたらしました。ついていけないランタイムのために、core-js はポリフィル(features/typed-array/to-base64 / from-base64)を配布し、小さな base64-js パッケージ(関数 3 つ、依存ゼロ)は、何年も黙々と推移的な依存としてエコシステムを支えてきました。

彼らが仕える形式は、それらすべてより古いです。このアルファベットは最初に 1987 年に Privacy-Enhanced Mail のために標準化され(RFC 989)、1993 年の改訂(RFC 1421)がそれを維持し、MIME が同じ年の数ヶ月後に 76 文字のラップとともに採用しました。RFC 3548 は 2003 年に Base-N ファミリーを統合し URL 安全バリアントを追加し、RFC 4648 が 2006 年にそれを再発行しました。10 年後、RFC 7515 と 7519 がパディングなしの base64url をすべての JWT の背骨にし、RFC 7636 がそれを OAuth の PKCE フローに入れました。この記事のエンコーダーは、30 年を数えながらまだ乗客を増やし続けている形式のラストマイルです。

パーティで披露する価値がある知識

  • btoa('GIF89a') は "R0lGODlh" を返し、GIF のマジックヘッダー全体が 8 文字で済みます。これはバイナリファイルが Base64 で言える最小の「こんにちは」であり、Wikipedia の記事で最初の Web API の例なのには理由があります。
  • toBase64() には btoa() が絶対に持てなかった omitPadding オプションがあります。Web API の契約は無条件にパディングするからです。同じアルファベットが 20 年続き、新しい API だけが古い API に決して許されなかったことを 1 つできます。
  • アルファベットは 1 つ、公式の行長は 2 つ:MIME は 76 で、PEM は 64 で折り返します。同じ 64 文字、同じパディング、テキストの行がどのくらい広くてもいいかという、2 つの 30 年物のご意見。
  • 33% という数字は正確です:3 バイトごとに 4 文字は 4/3 の比率で、RFC 時代のメールは改行でさらに約 3.5% を上乗せしました。あなたの「小さな」設定文字列は、何の対価もなく 37% 太っています。
  • 小さな base64-js パッケージは npm で毎週 1 億ダウンロード以上を集め、そのほとんどは他のパッケージの依存ツリーの奥に隠れています。Base64 は JavaScript エコシステムで最も密輸されるコードです。
  • 小さな Buffer は 1 つずつ割り当てられません。Node は共有の 65536 バイトのプール(Buffer.poolSize)から切り出します。Buffer の作成が速いのはこのためであり、前の住人のデータを気にする必要がない場合に備えて「unsafe」な割当てのバリアントが存在するのもこのためです。
  • 1998 年に data URL を定義した RFC は、HTML 属性の 1024 文字制限を引き合いに、それが「短い値でのみ有用である」と警告しています。モダンなブラウザはその同じ属性に、メガバイトサイズの画像を data URL として埋め込みます。あなたのヒーロー画像次第で、それは進歩なのか、傲慢さなのかです。
  • Unix のパスワードハッシュは、独自の Base64 フレーバーのアルファベットを使い、パディングはなく、混乱を招くことに、順目はスキームごとに違います。クラシックな crypt(3) ハッシュは ./0-9A-Za-z を使い、JavaScript プロジェクトがユーザーパスワードに保存する $2b$ bcrypt 文字列は、同じ 64 文字を ./A-Za-z0-9 に並べ替えています。セキュリティ文脈で「Base64」は単一の形式ではなくファミリーである、という良い提醒です。
  • Node のデコーダーは -、_、+、/ を 'base64' モードでも 'base64url' モードでも受け入れます。4 つの文字、1 つの表。エンコーダーは当然、あなたが要求した方言だけを話します。

往復の半分

JavaScript と Node.js での Base64 エンコードは、3 つの決断に集約されます:あなたはどのバイトを手にしているか(文字列には文字セットが必要、Buffer には不要)、目的地はどのアルファベットを要求するか(MIME と data URL にはクラシック、トークンと URL には base64url、パディングは文脈で任意)、そしてその形式がまだ強制している行規則は何か(メールは 76、PEM は 64、JSON はなし)。それらに答えれば、組み込み機能が残りを実行してくれます:Node では Buffer.toString()、ブラウザでは btoa() 加え UTF-8 の橋、そしてようやく 1 つ手に入れたモダンなランタイムでは Uint8Array.toBase64()。

そして、あなたがここで封じたすべての包みは、いつか誰かが開きます。デコード側には独自の罠のセットがあります:音も立てずにゴミを飲み込む寛容なデコーダー、atob() があなたに手渡すバイナリ文字列の仮面、壁の読者側で起きる文字セットの決断、そしてあなたがちょうど学んだ繰り越しパターンを模したストリーミングロジック。その物語は、各ステップにコード例を添えて、姉妹サイトの関連する Base64 デコード記事で詳しく扱われています。次はそれを読んでください。アルファベットのあの側では、罠はより静かで、静かであることがまさにそれらの勝ち方だからです。

最終更新: 2026-10-09

関連記事: JavaScript/Node.js での Base64 デコード:完全ガイド