Base64-Kodierung in JavaScript/Node.js: Ein vollständiger Leitfaden
Sie haben Daten, die zu Text werden müssen. Eine Datei, die in einem JSON-Feld mitfahren muss, ein Bild, das in einer CSS-Datei leben will, ein Secret, das in einer Umgebungsvariable sitzen wird, ein Token, das durch einen Query-String reisen wird. Die Antwort in JavaScript und Node.js ist fast immer dieselbe: Base64. Dieser Artikel ist die Packanleitung, vom ersten Byte, das Sie in der Hand halten, bis zum Moment, in dem Ihre kodierte Zeichenkette die Maschine verlässt.
Die Startseite dieser Site erklärt das Format in voller Detailtiefe, das Alphabet, die Mathematik, das Padding, also ist es hier ein einziger Satz: Aus je drei Bytes werden vier druckbare Zeichen, und deshalb wird Ihre Ausgabe etwa 33 Prozent größer sein als die Eingabe. Behalten Sie das in der Tasche, denn es ist der Grund, warum jeder Abschnitt dieses Artikels existiert, und es ist die Zahl, in der Ihre Speicher-Rechnung abgerechnet wird.
Der tröstliche Teil: Sie installieren nichts. Jeder moderne Browser liefert btoa() und das neuere Uint8Array.toBase64() mit, und jede Node.js-Version, die zählt, trägt die Buffer-Klasse mit einem 'base64'-Modus und, seit Version 15.7.0, einem erstklassigen 'base64url'-Modus. Die Kunst besteht darin zu wissen, welche Eingabeform Sie in der Hand halten, welches Alphabet das Ziel verlangt und welche Zeilenumbruch-Regeln die alten Formate noch immer durchsetzen.
Kennen Sie Ihre Eingabe, bevor Sie kodieren
Jede Kodierungs-Frage beginnt mit derselben: Was halten Sie genau in der Hand? Ein JavaScript-String ist UTF-16-Text, ein Buffer ist ein Byte-Array, und der richtige Aufruf hängt davon ab, welches von beiden Sie haben:
| Sie halten | Rufen Sie das auf | Notizen |
|---|---|---|
| Eine nur-ASCII-Zeichenkette (Zeichen unter 256) | btoa(string) |
Der schnellste Weg in Browsern und Node.js 16+, aber er stoppt beim ersten Zeichen, das nicht in ein Byte passt |
| Jede Unicode-Zeichenkette | TextEncoder zu Bytes, dann ein base64-Aufruf |
Die UTF-8-Brücke; der einzige sichere Weg für Buchstaben mit Akzentzeichen und Emoji |
| Ein Buffer oder Uint8Array | buffer.toString('base64') oder bytes.toBase64() |
Das Arbeitstier von Node.js, und die ES2026-Methode in modernen Browsern und Node.js 25+ |
Drei Beispiele, eines pro Zeile der Tabelle:
// Nur-ASCII-Text: der klassische Kurzweg (Browser und Node.js 16+)
console.log(btoa('hello world')); // "aGVsbG8gd29ybGQ="
// Beliebiger Text in Node.js: Buffer liest standardmäßig UTF-8
const { Buffer } = require('node:buffer');
console.log(Buffer.from('héllo ⛳', 'utf8').toString('base64')); // "aMOpbGxvIOKbsw=="
// Bytes, die Sie bereits besitzen
console.log(Buffer.from([1, 2, 3, 4]).toString('base64')); // "AQIDBA=="
console.log(new Uint8Array([1, 2, 3, 4]).toBase64()); // "AQIDBA==" (ES2026-Runtimes)
Achten Sie auf das zweite Beispiel: derselbe Text erzeugt eine andere Base64-Zeichenkette, je nachdem, in welchem Zeichensatz Sie ihn kodieren. Das ist kein Bug - es ist das ganze Spiel. Die Base64-Ebene kodiert Bytes, und ein String wird erst dann zu Bytes, wenn Sie einen Zeichensatz gewählt haben, und so bedeutet "kodiere diesen Text" immer still und leise "kodiere die UTF-8-Bytes dieses Textes" (oder die Latin-1-Bytes, wenn Sie das so sagen).
Die Unicode-Mauer und die Brücken über sie
btoa() ist die älteste API im Raum, und ihr Vertrag ist ein aus den 1990ern: Jedes Zeichen der Eingabe-Zeichenkette muss in ein einzelnes Byte passen, Codepunkte zwischen 0 und 255. Alles darüber, ein Emoji, ein kyrillischer Buchstabe mit Akzent, ein chinesisches Zeichen, wirft:
try {
btoa('héllo ⛳');
} catch (error) {
console.log(error.name); // "InvalidCharacterError"
console.log(error.message); // "Invalid character" in Node; in Browsern die Latin1-Bereichs-Formulierung
}
Die Lösung ist, aufzuhören, in Zeichen zu denken, und anzufangen, in Bytes zu denken. TextEncoder (ein Global in jedem Browser und in Node.js) verwandelt den String in seine UTF-8-Byte-Folge; Sie heben diese Bytes in einen Latin-1-String, und btoa() bekommt genau das, was es zugesagt hat zu verarbeiten:
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, dieselben Bytes
Auch das ältere Idiom werden Sie in Codebasen treffen, und im Inneren funktioniert es auf dieselbe Weise: btoa(unescape(encodeURIComponent(text))). Der Aufruf von encodeURIComponent erzeugt percent-kodierte UTF-8-Bytes, und unescape verwandelt die percent-Escapes wieder in rohe Zeichen. Sowohl escape als auch unescape sind Legacy-Funktionen, also sollte neuer Code die TextEncoder-Brücke bevorzugen, aber wenn Sie die alte Form erben, wissen Sie jetzt genau, was sie tut, statt die Schultern zu zucken.
In Node.js ist die Mauer größtenteils keine große Sache, denn Buffer.from(text) setzt UTF-8 voraus und erledigt die Byte-Conversion für Sie im selben Aufruf. Am meisten zählt die Brücke im Browser, wo btoa() die Legacy-Option ist und der UTF-8-Schritt ausdrücklich von Ihnen zu leisten ist.
Bytes rein, Buchstaben raus: Buffer, Padding und Varianten
Sobald Sie Bytes haben, ist die Kodierungs-Seite von Node.js eine einzige Methode: toString('base64'). Sie übernimmt die Gruppen-Mathematik, das Padding, alles, und sie produziert immer kanonische Ausgabe im Sinne von RFC 4648, was bedeutet, dass die ungenutzten Pad-Bits der letzten Gruppe null sind:
const { Buffer } = require('node:buffer');
const fox = Buffer.from('The quick brown fox jumps over the lazy dog');
console.log(fox.length); // 43 Bytes
console.log(fox.toString('base64')); // "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=="
console.log(fox.toString('base64').length); // 60 Zeichen, die 33-Prozent-Steuer in Aktion
Das Padding am Ende leistet echte Arbeit, es ist keine Dekoration. Eine letzte Gruppe mit einem übrig gebliebenen Byte wird zu zwei Base64-Zeichen plus zwei =, und eine Gruppe mit zwei übrig gebliebenen Bytes wird zu drei Zeichen plus einem =. Ob Ihre Ausgabe dieses Padding tragen darf, hängt vom Ziel ab, und genau das ist der Unterschied zwischen den beiden Base64-Varianten, die Sie täglich verwenden werden:
const one = new Uint8Array([72]);
console.log(one.toBase64()); // "SA==" (ES2026, Padding inklusive)
console.log(one.toBase64({ omitPadding: true })); // "SA"
console.log(Buffer.from([72]).toString('base64url')); // "SA", Node lässt das Padding im base64url-Modus weg
Denken Sie an das Verhältnis, wenn Sie Größen planen: drei Bytes rein, vier Zeichen raus, sodass 1 MB Daten zu etwa 1,33 MB Text werden, und wenn Sie den Text für E-Mail oder PEM in Zeilen umbrechen, addieren die Zeilenumbrüche noch ein paar Prozent obendrauf.
Bytes über den Draht schicken
Das häufigste Draht-Problem in JavaScript ist, dass JSON keine Bytes hat. Es hat Strings, und der String, der sicher durch jeden JSON-Parser, jeden HTTP-Proxy und jedes Logging-System reisen kann, ist der Base64. Das Muster ist auf beiden Enden der Verbindung dasselbe: an der Grenze kodieren, an der Grenze dekodieren, dazwischen Bytes halten:
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, die Datei fährt jetzt in ganz gewöhnlichem JSON mit
Wissen Sie, wann Sie gegen dieses Muster kämpfen sollten. Wenn Ihr Transport bereits Binär unterstützt, nutzen Sie ihn: Ein multipart/form-data-Upload schickt die rohe Datei ohne Größensteuer, ein WebSocket-Frame trägt rohe Bytes, und eine Postgres-bytea-Spalte speichert sie nativ. Base64 an einem Ort, an dem rohe Bytes erlaubt waren, ist reiner Overhead, die 33-Prozent-Steuer ohne jede Ausbeute. Base64 verdient sein Geld, wenn der Kanal nur Text ist: JSON-APIs, E-Mail-Körper, Umgebungsvariablen, URL-Query-Strings und die vielen Brücken (Mobile-SDKs, Desktop-Apps, Chat-Systeme), die nur Text durchlassen.
Data-URLs: Bilder, die in Text leben
Die Data-URL, der data:image/png;base64,...-String, ist Base64 mit einem MIME-Label, und sie ist der Grund, warum Sie ein ganzes Bild in ein einzelnes HTML-Attribut packen können. Der RFC aus dem Jahr 1998, der das Schema definierte, sagt sogar, es sei "nur für kurze Werte nützlich", weil frühes HTML eine 1024-Zeichen-Grenze für Attribut-Werte hatte. Moderne Browser lachen über diese Grenze und rendern freudig Data-URLs in Megabyte-Größe, was sowohl Superkraft als auch Falle ist.
Im Browser erledigt die canvas-API den gesamten Job für Sie, Pixel rein, Data-URL raus:
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"
Und in jeder Runtime, einschließlich Node.js, ist das Bauen einer Data-URL nur String-Konkatenation mit den Metadaten am richtigen Ort: ein data:-Präfix, der Medientyp, der optionale ;base64-Marker, ein Komma und der Payload. Ohne den ;base64-Marker wird der Payload stattdessen als percent-kodierter Text erwartet, und genau deshalb existiert der Marker:
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
Die ehrlichen Abwägungen: Eine Data-URL ist Teil des Dokuments, also ist sie nicht als eigenständige Ressource cachelbar, sie zählt gegen die Größe des HTML oder CSS, in dem sie sitzt, und das DOM muss sie parsen und halten. Für ein 20-Kilobyte-Icon ist das ein Schnäppchen. Für ein 4-Megabyte-Hero-Bild schicken Sie die Datei über HTTP, wo Caching und Kompression beide funktionieren, und behalten die Data-URL für die kleinen Dinge.
Dateien, die als Strings reisen
Datei zu Base64 ist ein Zwei-Schritte-Tanz, den beide Runtimes in einen einzigen Aufruf komprimieren. In Node.js akzeptiert das Dateisystem 'base64' als Lese-Kodierung, und die Schreib-Seite akzeptiert sie ebenfalls:
const fs = require('node:fs');
const { Buffer } = require('node:buffer');
const base64 = fs.readFileSync('./report.pdf', 'base64');
console.log(base64.length); // die Datei, etwa 33 Prozent schwerer
fs.writeFileSync('./report.pdf.b64', base64, 'utf8');
const copy = Buffer.from(base64, 'base64');
fs.writeFileSync('./report.copy.pdf', copy);
Im Browser erledigt der FileReader denselben Job, mit einem Twist: Sein Datenlese-Modus übergibt Ihnen eine Data-URL, also schneiden Sie das Präfix ab, um den nackten Base64-Payload zu bekommen:
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); // die Datei, bereit für eine JSON-Anfrage
};
reader.readAsDataURL(fileInput.files[0]);
});
Wenn Sie im Browser das rohe Base64 ohne das Data-URL-Präfix brauchen, überspringt file.arrayBuffer() gefolgt von Uint8Array.toBase64() (auf Runtimes, die es haben) das Präfix ganz und ist der sauberere Weg für Upload-Pipelines.
base64url: Das Alphabet, das URLs überlebt
Klassisches Base64 trägt zwei Zeichen, gegen die URLs allergisch sind. Das + wird zu einem Leerzeichen, wann immer ein Query-String form-dekodiert wird, das / ist ein Pfadtrenner, und das =-Padding sieht aus wie eine Zuweisung. Die URL- und Dateinamen-sichere Variante aus RFC 4648, Abschnitt 5, base64url, tauscht die beiden Sonderzeichen gegen - und _ aus und lässt das Padding fallen, wann immer die Länge aus dem Kontext bekannt ist. Es ist das Alphabet von JWTs, OAuth-Tokens und Deep Links, und es verdient einen festen Platz in Ihrem geistigen Werkzeugkasten.
Der Buffer von Node spricht diesen Dialekt seit Version 15.7.0, und die Kodierungs-Seite ist ein einzelnes Argument:
const { Buffer } = require('node:buffer');
const classic = 'k+XS/B4=';
console.log(Buffer.from(classic, 'base64').toString('base64url')); // "k-XS_B4"
Zwei Dinge fallen auf. Das + wurde zu einem -, das / zu einem _, und das Padding ist verschwunden, denn der base64url-Modus lässt es von Design aus weg. Und die IETF stellt explizit klar, dass dies eine andere Kodierung ist, nicht dieselbe im Kostüm, und wenn also eine Spezifikation "base64url" sagt, sollten Sie base64url produzieren, nicht klassisches Base64 mit Suchen-und-Ersetzen. Die ES2026-Methode macht dieselben Entscheidungen zu expliziten Optionen, und ihr omitPadding-Flag gibt Ihnen das Padding zurück, wenn der Kontext es verlangt:
console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url', omitPadding: true })); // "q_8"
console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url' })); // "q_8=", zwei Bytes brauchen ein Pad-Zeichen
Auf Runtimes ohne beides ist die Konversion ein Zwei-Zeichen-Tausch plus ein Padding-Trimmen, und sie ist eines der am meisten kopierten Snippets der JavaScript-Welt:
const toUrlSafe = (value) => value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
console.log(toUrlSafe('k+XS/B4=')); // "k-XS_B4"
Verwenden Sie base64url für alles, was in einer URL, einem Query-String, einem Dateinamen oder einem Token-Standard leben wird. Verwenden Sie klassisches Base64 für MIME-Körper, Data-URLs und alles, was niemals einen percent-Decoder treffen wird. Beides zu verwechseln ist der häufigste Interop-Bug in diesem gesamten Format.
Zugangsdaten versiegeln: Basic Auth, JWTs und PKCE
Drei Authentifizierungs-Ecken des Web sind auf Base64 gebaut, und alle drei sind einmalig günstig per Hand zu bauen, was eine gute Sache ist, denn zu wissen, was unter der Library passiert, ist es, was Sie ruhig hält, wenn die Library Sie überrascht.
Erstens, HTTP-Basic-Authentifizierung (RFC 7617): Der Client sendet das Schema-Wort Basic plus das Base64 von user-id:password. Eine Zeile, und eine ernste Warnung angehängt:
const { Buffer } = require('node:buffer');
console.log('Basic ' + Buffer.from('octo:cat').toString('base64')); // "Basic b2N0bzpjYXQ="
Base64 hier ist Obskurierung, keine Sicherheit. Jeder, der den Header lesen kann, kann das Passwort lesen, also ist dieses Schema nur über HTTPS akzeptabel, und selbst dann ist es ein Legacy-Muster: Bevorzugen Sie Tokens. Zweitens, das JWT: Die ersten zwei durch Punkte getrennten Teile sind base64url von klarem JSON, und der dritte ist die Signatur. Ein HMAC-SHA256-Token per Hand zu bauen ist ein Handvoll Zeilen des eingebauten crypto-Moduls:
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 Teile, durchgehend base64url ohne Padding
Achten Sie auf die Details, die ein Token machen oder brechen: nirgendwo Padding (RFC 7515 lässt es weg), die Signatur wird über die wörtliche Zeichenkette header + '.' + payload berechnet, nicht über die geparsten Objekte, und das Ganze ist nur so geheim wie der Key. In der Produktion werden Sie eine Library verwenden, jose (null Abhängigkeiten, Browser und Node.js) oder jsonwebtoken (Node.js), aber sie führen diese exakten Aufrufe unter der Haube aus. Drittens, PKCE (RFC 7636), die Erweiterung, die öffentlichen Clients wie SPAs und mobile Apps sicheres Einloggen erlaubt: Der Client erzeugt einen hoch-entropischen code_verifier, veröffentlicht BASE64URL(SHA256(verifier)) als Challenge und beweist den Besitz des Verifiers beim Token-Austausch. Zufälligkeit zählt, also kommt der Verifier aus dem crypto-Modul, niemals aus 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, beide innerhalb des erlaubten Bereichs von 43-128
Alte Post braucht Verpackung: MIME- und PEM-Rüstung
Zwei der ältesten Base64-Formate der Welt setzen noch immer Zeilenlängen durch, und beide sind etwa 30 Jahre alt. MIME, der E-Mail-Standard aus RFC 2045, bricht sein Base64 bei 76 Zeichen pro Zeile um und verlangt, dass die Zeilen mit CRLF enden, ein Relikt der 8-bit-clean-SMTP-Tage, als sehr lange Zeilen echte Mail-Server kaputt machten. RFC 7468, der die PEM-Regeln für Zertifikate und Keys aufschreibt, ist noch strenger: Erzeuger müssen bei exakt 64 Zeichen pro Zeile umbrechen, die letzte Zeile kürzer, gerahmt von -----BEGIN- und -----END-Rüstungszeilen, die den Inhalt benennen.
Das Umbrechen selbst ist ein Einzeiler, und die Rüstung eine Vorlage:
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 Zeichen
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-----
Zwei praktische Hinweise. Wenn Sie MIME oder PEM erzeugen, brechen Sie es um, denn strenge Konsumenten (Mail-Gateways, OpenSSL-Ära-Tools, Java-Key-Stores) werden einen 4000-Zeichen-Einzeiler-Base64-Blob verwerfen. Wenn Sie es konsumieren, müssen Sie das in der Regel nicht, denn der Decoder von Node überspringt Leerraum, also übernimmt Buffer.from die Zeilenumbrüche für Sie - aber die Rüstungszeilen selbst sind kein Base64, also entfernen Sie die -----BEGIN/-----END-Zeilen (oder matchen Sie nur den Körper) vor dem Dekodieren: Buffer.from(pem.replace(/-----[A-Z ]+-----/g, ''), 'base64'). Diese Asymmetrie ist ein Geschenk, aber es bedeutet nicht, dass Sie den Schritt des Rüstungs-Entfernens überspringen können, wenn das Base64 irgendwohin geht, das nichts überspringt, wie einen DER-Parser.
Wo kodierte Daten wohnen: Env, Config und Datenbanken
Base64 ist auch ein Speicher-Format, was bequem ist und gefährlich leicht für Sicherheit gehalten werden kann. Umgebungsvariablen sind die klassische Heimat: mehrere Secret-Manager und CI-Systeme übergeben Ihnen Base64-kodierte Werte, und das Dekodieren ist ein Einzeiler:
const { Buffer } = require('node:buffer');
const stored = process.env.API_KEY_B64; // "c3VwZXItc2VjcmV0"
console.log(Buffer.from(stored, 'base64').toString('utf8')); // "super-secret"
Sprechen Sie den wichtigen Satz laut aus: Kodierung ist keine Verschlüsselung. Ein Base64-"Secret" in einer Umgebungsvariable, einer .env-Datei oder einem Kubernetes-Secret (k8s speichert seine Secrets als Base64 in der API und in etcd, und die Dokumentation wiederholt es ständig) ist für jeden lesbar, der die Prozess-Umgebung, die Datei oder den Cluster lesen kann. Verwenden Sie dort Base64, weil der Transport (Shell, YAML, JSON) nur Text ist, niemals, weil Sie glauben, es verberge etwas.
In Datenbanken ist Base64 die Standard-Brücke für Binär innerhalb von JSON-Dokument-Speichern, denn eine jsonb-Spalte oder ein MongoDB-Dokument hat keinen eigenen Byte-Typ:
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=="}
Speichern Sie den Medientyp neben dem Payload, wie das Beispiel es tut, und Sie werden sich in einem Jahr dafür bedanken, wenn jemand fragt, was die Bytes sind. Wenn Ihre Datenbank einen nativen Binär-Typ hat (Postgres bytea ist das Referenz-Beispiel), bevorzugen Sie ihn: Die Bytes kosten nichts extra, und Sie sparen die 33-Prozent-Steuer für immer.
Streams kodieren, ohne Gruppen zu zerreißen
Base64 arbeitet in Drei-Byte-Gruppen, also muss ein Kodierer, der beliebige Chunks erhält, seinen Rest mitnehmen: ein oder zwei Bytes, die noch keine Gruppe bilden können, müssen auf den nächsten Chunk warten, bevor sie kodiert werden können. Machen Sie die Mathematik pro Chunk und emittieren Sie nur vollständige Gruppen, und die Ausgabe ist Byte-identisch mit dem Kodieren des gesamten Streams auf einmal:
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"; identisch mit einem großen toString('base64')
});
encoder.end(Buffer.from('hello world, this is a stream!'));
Der flush-Callback ist das Detail, das alle vergessen: Die letzten ein oder zwei Bytes, die nie einen Partner in einem regulären Chunk gefunden haben, bekommen ihr Padding und werden am Ende herausgeschoben. Dasselbe Mitnehmen-Prinzip ist es, das Sie auf der Dekodierungs-Seite spiegeln werden, nur dass Ihnen dort die ES2026-API es gratis gibt: setFromBase64() mit "stop-before-partial" stoppt exakt an Gruppengrenzen und sagt Ihnen, wie viele Zeichen es verbraucht hat.
Große Dateien und die Speicher-Rechnung
Base64 ist großzügig mit Platz, also brauchen große Dateien eine Strategie. Eine 1-GB-Datei wird zu etwa 1,33 GB Base64-Text, und ein JavaScript-String speichert UTF-16, zwei Heap-Bytes pro Zeichen, also verlangt dieser Text allein grob 2,7 GB Speicher, bevor Ihr Buffer ankommt. Die Obergrenze ist in Node explizit: buffer.constants.MAX_STRING_LENGTH sind 536870888 Zeichen, knapp unter 512 MiB Text, die zu etwa 400 MB Bytes dekodieren. Darüber hinaus ist ein einzelner String keine Option, und Streaming ist das einzige Spiel in der Stadt:
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');
});
Das Muster ist der Stream-Kodierer aus dem vorherigen Abschnitt, abgeflacht: in 64-KiB-Chunks lesen, den 1-bis-2-Byte-Rest mitnehmen, vollständige Gruppen emittieren, den Schwanz flushen. Die Speicher-Spur bleibt bei etwa einem Chunk plus einem Rest, egal wie schwer die Datei ist. Und wenn die empfangende Seite Binär akzeptieren kann, fragen Sie sich, warum Sie die Steuer überhaupt zahlen.
Einzeiler für das Terminal
Node dient auch als Base64-Kodierer für die Kommandozeile, was praktisch ist, wenn Sie einen Config-Wert verpacken, eine API debuggen oder eine kleine Datei per Chat-Nachricht zwischen Maschinen schicken:
# Eine Datei zu klassischem Base64 auf stdout kodieren
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64"))' notes.txt
# Die URL-sichere Variante, Padding entfernt
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64url"))' notes.txt
# Von stdin lesen, wofür Pipes da sind
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")))'
Keiner der drei fügt von sich aus einen abschließenden Zeilenumbruch hinzu, was die Ausgabe für Copy-Paste und für $(...)-Ersetzung in Shell-Skripten sauber hält. Wenn Sie eine hübsch formatierte Datei mit umgebrochenen Zeilen wollen, leiten Sie das Ergebnis in Ihren Editor der Wahl, oder fügen am Ende des Einzeilers ein \n hinzu.
Stolperfallen, die Entwicklern Stunden gekostet haben
Jede einzelne davon hat in einer echten Codebase einen echten Nachmittag gekostet:
- Die Unicode-Mauer:
btoa('héllo ⛳')wirftInvalidCharacterError, weil die Golf-Flagge nicht in ein Byte passt. Die Lösung ist die UTF-8-Brücke: erstTextEncoderzu Bytes, dann kodieren. In Node.js umgehen Sie das Problem ganz mitBuffer.from(text), das UTF-8 setzt voraus. - Das Legacy-Idiom: Alter Code voller
btoa(unescape(encodeURIComponent(x)))funktioniert, aberescapeundunescapesind deprecated Legacy-Funktionen. Wenn Sie diesen Code refactorieren, ersetzen Sie ihn durch dieTextEncoder-Brücke, und das Verhalten bleibt identisch. - Das fehlende Encoding-Argument, in umgekehrter Richtung: Der Stolperfall auf der Dekodierungs-Seite ist
Buffer.from(str)ohne den 'base64'-Modus; sein Zwilling auf der Kodierungs-Seite ist die Annahme,Buffer.from(someString)tue etwas Besonderes mit Base64. Tut es nicht. Ohne explizite Kodierung baut er einen Buffer aus den UTF-8-Bytes des Strings, und Ihre "kodierte" Ausgabe ist das Base64 der Buchstaben-Bytes des Strings, was fast nie das Gewünschte war. Seien Sie in beiden Richtungen explizit. - Die Padding-Fehlanpassung: JWTs und die meisten Token-Standards wollen base64url ohne Padding, MIME will klassisches Base64 mit Padding, und die zwei sind leicht zu verwechseln. Ein gepaddetes
=innerhalb eines JWT-Teils bricht strenge Verifizierer; fehlendes Padding, wo die Länge unbekannt ist, bricht nachsichtige Dekoder. Passen Sie sich dem Standard an, nicht Ihrer Gewohnheit. - Der nicht-kanonische Schwanz: RFC 4648 verlangt, dass die ungenutzten Pad-Bits der letzten Gruppe null sind. Die eingebauten Kodierer produzieren alle kanonische Ausgabe, aber ein handgebauter Kodierer, der Bits von Hand verschiebt, kann Müll in diesen Bits hinterlassen, und ein strenger Dekoder wird Ihren Payload ohne ersichtlichen Grund verwerfen. Wenn Sie Ihren eigenen Kodierer schreiben, testen Sie gegen die RFC-4648-Testvektoren, nicht nur gegen Ihre eigenen Daten.
- Das vergessene Umbrechen: MIME will Zeilen mit 76 Zeichen und PEM will 64, und strenge Konsumenten (Mail-Gateways, Java-Key-Tools) verwerfen einen Einzeiler-Blob. Das Gegenteil ist seltener, aber real: Einige Parser sind zeilenorientiert, und ein fehlendes CRLF am Ende einer PEM-Datei hat mehr Builds kaputt gemacht als jeder Bug im Base64 selbst.
- Die Sicherheits-Illusion: Base64 in einer Umgebungsvariable, einer
.env-Datei oder einem Kubernetes-Secret ist keine Verschlüsselung. Es dekodiert mit einer Zeile Code, in jeder Sprache, von jedem, der die Datei oder den Cluster lesen kann. Behandeln Sie es als Transport-Kostüm und lassen Sie die echten Kontrollen (Berechtigungen, TLS, Key-Rotation) die Schutzarbeit machen. - Die JSON-Aufblähung: Base64 innerhalb von JSON kostet 33 Prozent plus Escaping, und ein 5-MB-Upload wird zu einem 6,7-MB-String, den Ihr JSON-Parser in den Speicher kopieren muss. Für alles in Dateigröße über HTTP ist
multipart/form-dataoder ein roher binärer Körper der bessere Transport, und Base64 ist für den Fall, dass der Kanal nur Text ist. - Die Heap-Rechnung: Ein kodierter String ist UTF-16 im JavaScript-Heap, zwei Bytes pro Zeichen, und der Buffer, hier die Quelle, dort das Dekodier-Ergebnis, ist eine zweite Kopie der Daten. Eine 100-MB-Datei bedeutet kurzzeitig etwa 270 MB String plus 100 MB Buffer. Streamen Sie die großen, und halten Sie die kodierte Form referenziert so kurz, wie der Code es erlaubt.
- Die Legacy-Globals in Node: Die eigene Dokumentation von Node markiert
btoa()undatob()als Stability 3, Legacy, und sagt Ihnen, stattdessenBufferzu verwenden. In einem Browser istbtoa()ein vollkommen gutes Werkzeug für ASCII-Text; in Node.js greifen Sie zum Buffer und lassen Sie die Globals dem polyfill-förmigen Code, der sie braucht.
Wie JavaScript lernte, Bytes zu verpacken
Die Browser-Seite ist eine lange, stille Geschichte. btoa() wurde Anfang 2011 im HTML5-Entwurf spezifiziert und sitzt seit Mitte der 2000er in jedem großen Browser, unverändert im Verhalten, mit ihrem einen-Byte-pro-Zeichen-Vertrag und ihrer immer-gepaddeten Ausgabe. Dieser Vertrag ist älter als Typed Arrays - Binär-Strings waren vor 2009 der einzige Weg, Bytes zu tragen -, und deshalb denkt btoa() noch immer in "Binär-Strings". Die moderne Hälfte der Geschichte ist sehr jung: Der TC39-Vorschlag, der natives Base64 zu Typed Arrays hinzufügte (zusammen mit hex), wurde als Teil von ES2026 standardisiert und landete 2024 in Firefox 133 und Safari 18.2, am 2. September 2025 in Chrome 140, und wurde dann als Baseline Newly available erklärt. Bun lieferte dieselben Methoden in Version 1.1.22 im August 2024.
Node.js packte Bytes nach einem anderen Takt. Die Buffer-Klasse wurde in Version 0.1.103, im Sommer 2010, fast fünf Jahre vor Node 1.0, zu einem Global, und toString('base64') war über ein Jahrzehnt lang der Kodierer der Wahl, mit den Alphabet-Eigenheiten jener Ära (er akzeptierte beim Dekodieren bereits die URL-sicheren Zeichen, eine zweisprachige Gewohnheit, die die Spezifikation nie verlangt hat). Version 15.7.0 im Januar 2021 fügte den 'base64url'-Modus als erstklassigen Kodierungsnamen hinzu, Node 16 im selben Jahr fügte die Browser-Globals btoa()/atob() hinzu (sofort als Legacy markiert), und Node 22 im Jahr 2024 lieferte weitere V8- und base64-Performance-Arbeit. Dann Node 25, veröffentlicht am 15. Oktober 2025, upgradete V8 auf 14.1 und brachte die ES2026-Methoden, toBase64() mit seiner omitPadding-Option und setFromBase64() für die andere Richtung, in die Runtime. Für Runtimes, die nicht mithalten können, liefert core-js Polyfills (features/typed-array/to-base64 / from-base64), und das kleine base64-js-Paket (drei Funktionen, null Abhängigkeiten) hat das Ökosystem Jahre lang still als transitive Abhängigkeit getragen.
Das Format, das sie bedienen, ist älter als all das. Das Alphabet wurde 1987 erstmals für Privacy-Enhanced Mail standardisiert (RFC 989), die Revision von 1993 (RFC 1421) behielt es bei, und MIME übernahm es wenige Monate später im selben Jahr mit seinem 76-Zeichen-Umbruch; RFC 3548 konsolidierte die Base-N-Familie 2003 und fügte die URL-sichere Variante hinzu, die RFC 4648 2006 neu herausgab. Ein Jahrzehnt später machten RFC 7515 und 7519 das padding-freie base64url zum Rückgrat jedes JWT, und RFC 7636 brachte es in OAuths PKCE-Flow. Die Kodierer in diesem Artikel sind die letzte Meile eines Formats, das dreißig Jahre alt ist und noch immer Passagiere gewinnt.
Auf einer Party erwähnenswert
btoa('GIF89a')liefert"R0lGODlh", den gesamten magischen Header eines GIFs in acht Zeichen. Es ist das kleinste "Hallo", das eine binäre Datei in Base64 sagen kann, und es ist aus gutem Grund das erste Web-API-Beispiel im Wikipedia-Artikel.toBase64()hat eineomitPadding-Option, diebtoa()nie haben konnte, denn der Web-API-Vertrag padet bedingungslos. Zwei Jahrzehnte desselben Alphabets, und die neuere API kann eine Sache, die die ältere nie durfte.- Ein Alphabet, zwei offizielle Zeilenlängen: MIME bricht bei 76 um, PEM bei 64. Dieselben 64 Zeichen, dasselbe Padding, zwei verschiedene, 30 Jahre alte Meinungen darüber, wie breit eine Textzeile sein darf.
- Die 33-Prozent-Zahl ist exakt: vier Zeichen pro drei Bytes ist ein 4/3-Verhältnis, und E-Mail der RFC-Ära fügte für die Zeilenumbrüche grob weitere 3,5 Prozent hinzu. Ihr "kleiner" Konfigurations-String ist 37 Prozent dicker, für nichts.
- Das kleine
base64-js-Paket zieht auf npm über 100 Millionen Downloads pro Woche an, fast alles versteckt in den Abhängigkeits-Bäumen anderer Pakete. Base64 ist der am meisten geschmuggelte Code im JavaScript-Ökosystem. - Kleine Buffer werden nicht einzeln allokiert: Node schnitzt sie aus einem gemeinsamen 65536-Byte-Pool (
Buffer.poolSize), und deshalb ist die Buffer-Erzeugung schnell, und deshalb existieren die "unsafe"-Allokations-Varianten für die Fälle, in denen die Daten des Vormieters egal sind. - Der RFC, der Data-URLs 1998 definierte, warnt, sie seien "nur für kurze Werte nützlich", und zitiert eine 1024-Zeichen-Grenze für HTML-Attribute. Moderne Browser betten Megabyte-große Bilder als Data-URLs in dieselben Attribute ein, was entweder Fortschritt oder Überheblichkeit ist, je nach Ihrem Hero-Bild.
- Unix-Passwort-Hashes verwenden ihre eigenen Base64-flavorierten Alphabete, ohne Padding, und, verwirrenderweise, unterscheidet sich die Reihenfolge je nach Schema: Klassische
crypt(3)-Hashes verwenden./0-9A-Za-z, während die$2b$-bcrypt-Strings, die JavaScript-Projekte für Benutzer-Passwörter speichern, dieselben 64 Zeichen stattdessen zu./A-Za-z0-9mischen. Eine gute Erinnerung daran, dass "Base64" in einem Sicherheits-Kontext eine Familie ist, nicht ein einzelnes Format. - Der Decoder von Node akzeptiert
-,_,+und/in beiden Modi,'base64'und'base64url', vier Zeichen, eine Tabelle. Der Kodierer spricht natürlich nur den Dialekt, den Sie gewünscht haben.
Die Hälfte einer Rundreise
Base64-Kodierung in JavaScript und Node.js kommt auf drei Entscheidungen an: Welche Bytes halten Sie (ein String braucht einen Zeichensatz, ein Buffer keinen), welches Alphabet verlangt das Ziel (klassisch für MIME und Data-URLs, base64url für Tokens und URLs, Padding je nach Kontext optional) und welche Zeilen-Regeln setzt das Format noch durch (76 für E-Mail, 64 für PEM, keine für JSON)? Beantworten Sie diese, und die Built-ins erledigen den Rest: Buffer.toString() in Node, btoa() plus die UTF-8-Brücke im Browser, und Uint8Array.toBase64() in den modernen Runtimes, die endlich eines bekommen haben.
Und jedes Paket, das Sie hier versiegeln, wird irgendwann jemand anderes öffnen. Die Dekodierungs-Seite hat ihre eigene Sammlung an Fallen: der nachsichtige Decoder, der Müll ohne ein Geräusch verschluckt, das Binär-String-Kostüm, das atob() Ihnen austeilt, die Zeichensatz-Entscheidungen, die auf der Leser-Seite der Mauer passieren, und die Streaming-Logik, die das Mitnehmen-Muster spiegelt, das Sie gerade gelernt haben. Diese Geschichte, mit Code-Beispielen für jeden Schritt, ist im Detail im verwandten Artikel zur Base64-Dekodierung auf unserer Schwester-Site abgedeckt. Lesen Sie ihn als Nächstes, denn die Fallen auf der anderen Seite des Alphabets sind leiser, und leise ist genau der Weg, wie sie gewinnen.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Dekodierung in JavaScript/Node.js: Ein vollständiger Leitfaden