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 reisen muss, ein Bild, das in einer CSS-Datei leben will, ein Geheimnis, das in einer Umgebungsvariablen 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 das Verpackungs-Handbuch, vom ersten Byte in Ihrer Hand bis zum Moment, in dem Ihr kodierte String die Maschine verlässt.
Die Startseite dieser Site erklärt das Format in voller Detailtiefe, das Alphabet, die Mathematik, das Padding, also hier in einem einzigen Satz: Aus jeweils drei Bytes werden vier druckbare Zeichen, deshalb wird Ihre Ausgabe etwa 33 Prozent größer sein als die Eingabe. Behalten Sie das in der Hosentasche, denn es ist der Grund, warum jeder Abschnitt dieses Artikels existiert, und es ist die Zahl, mit der Ihre Speicher-Rechnung berechnet wird.
Der tröstliche Teil: Sie installieren nichts. Jeder moderne Browser liefert btoa() und das neuere Uint8Array.toBase64(), 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 Eingabe-Form Sie in der Hand halten, welches Alphabet das Ziel verlangt und welche Zeilen-Umbau-Regeln die alten Formate noch durchsetzen.
Die Eingabe kennen, bevor Sie kodieren
Jede Kodierungsfrage beginnt mit derselben Frage: Was genau halten Sie 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 dies auf | Anmerkungen |
|---|---|---|
| Ein nur-ASCII-String (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 |
| Ein beliebiger Unicode-String | TextEncoder zu Bytes, dann ein base64-Aufruf |
Die UTF-8-Brücke; der einzige sichere Weg für Akzente und Emoji |
| Ein Buffer oder Uint8Array | buffer.toString('base64') oder bytes.toBase64() |
Das Node.js-Arbeitstier, und die ES2026-Methode in modernen Browsern und Node.js 25+ |
Drei Beispiele, eines pro Zeile der Tabelle:
// Nur-ASCII-Text: der Legacy-Shortcut (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 schon besitzen
console.log(Buffer.from([1, 2, 3, 4]).toString('base64')); // "AQIDBA=="
console.log(new Uint8Array([1, 2, 3, 4]).toBase64()); // "AQIDBA==" (ES2026-Runtimes)
Beachten Sie das zweite Beispiel: Derselbe Text erzeugt je nach Zeichensatz, in dem Sie ihn kodieren, einen anderen Base64-String. 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, also bedeutet "kodiere diesen Text" immer im Stillen "kodiere die UTF-8-Bytes dieses Textes" (oder die Latin-1-Bytes, wenn Sie es sagen).
Die Unicode-Wand und die Brücken über sie
btoa() ist die älteste API im Raum, und ihr Vertrag ist ein aus den 1990ern: Jedes Zeichen des Eingabe-Strings muss in ein einzelnes Byte passen, Codepunkte zwischen 0 und 255. Alles darüber, ein Emoji, ein akzentuierter kyrillischer Buchstabe, ein chinesischer Charakter, wirft:
try {
btoa('héllo ⛳');
} catch (error) {
console.log(error.name); // "InvalidCharacterError"
console.log(error.message); // "Invalid character" in Node; die Latin1-Bereichs-Formulierung in Browsern
}
Der Fix besteht darin, 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 überführen diese Bytes in einen Latin-1-String, und btoa() bekommt genau das, was es versprochen 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
Sie werden auch das ältere Idiom in Codebasen antreffen, und es funktioniert unter der Haube auf dieselbe Weise: btoa(unescape(encodeURIComponent(text))). Der encodeURIComponent-Aufruf erzeugt prozent-kodierte UTF-8-Bytes, und unescape verwandelt die Prozent-Escapes zurück in Roh-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 nur die Schultern zu zucken.
In Node.js ist die Wand weitgehend ein Nicht-Ereignis, denn Buffer.from(text) geht von UTF-8 aus und erledigt die Byte-Umwandlung für Sie im selben Aufruf. Die Brücke zählt am meisten im Browser, wo btoa() die Legacy-Option ist und der UTF-8-Schritt explizit von Ihnen zu vollziehen ist.
Bytes rein, Buchstaben raus: Buffer, Padding und Sorten
Sobald Sie Bytes haben, ist die Kodierungs-Seite von Node.js eine Methode: toString('base64'). Sie erledigt die Gruppen-Mathematik, das Padding, alles, und sie erzeugt immer kanonische Ausgabe im Sinne von RFC 4648, was bedeutet, dass die ungenutzten Padding-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 das ist der Unterschied zwischen den beiden Base64-Sorten, 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
Merken Sie sich das Verhältnis, wenn Sie Größen bemessen: drei Bytes rein, vier Zeichen raus, also werden aus 1 MB Daten etwa 1,33 MB Text, und wenn Sie den Text für E-Mail oder PEM in Zeilen umbrechen, kommen die Zeilenumbrüche obendrauf noch ein paar Prozent dazu.
Bytes über das Kabel schicken
Das häufigste Kabel-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 jede Logging-Anlage reisen kann, ist der Base64-String. Das Muster ist an beiden Enden der Verbindung dasselbe: an der Grenze kodieren, an der Grenze dekodieren, in der Mitte 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 reist jetzt im gewöhnlichen JSON
Wissen Sie, wann Sie gegen dieses Muster kämpfen sollten. Wenn Ihr Transport bereits Binäres unterstützt, nutzen Sie es: Ein multipart/form-data-Upload sendet die Roh-Datei ohne Größensteuer, ein WebSocket-Frame trägt Roh-Bytes, und eine Postgres-bytea-Spalte speichert sie nativ. Base64 an einer Stelle, an der Roh-Bytes erlaubt waren, ist reiner Overhead, die 33-Prozent-Steuer ohne etwas, das es zu zeigen hat. Base64 verdient seinen Lohn, wenn der Kanal nur Text erlaubt: 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 in einem MIME-Etikett, und sie ist der Grund, warum Sie ein ganzes Bild in ein einzelnes HTML-Attribut setzen können. Der RFC aus dem Jahr 1998, der das Schema definierte, sagt sogar, es sei "nur für kurze Werte nützlich", denn frühes HTML hatte eine 1024-Zeichen-Grenze für Attributwerte. Moderne Browser lachen über diese Grenze und rendern Data-URLs von Megabyte-Größe gerne, was sowohl eine Superkraft als auch eine Falle ist.
Im Browser erledigt die Canvas-API die ganze Arbeit 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 eines zu bauen nur String-Konkatenierung mit den Metadaten am richtigen Ort: ein data:-Präfix, der Medientyp, die optionale ;base64-Marke, ein Komma, und der Payload. Ohne die ;base64-Marke wird vom Payload erwartet, dass er prozent-kodierter Text ist, und das ist der Grund, warum die Marke existiert:
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 lässt sie sich nicht als eigenständige Resource cachen, sie zählt gegen die Größe des HTML oder der CSS, in der 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 versenden 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 Zweistufen-Tanz, den beide Runtimes in einen einzelnen Aufruf komprimieren. In Node.js akzeptiert das Dateisystem 'base64' als Lese-Kodierung, und die Schreib-Seite akzeptiert sie auch:
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 FileReader dasselbe, mit einem Twist: Sein Daten-Lese-Modus reicht 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 den rohen Base64 ohne Data-URL-Präfix brauchen, überspringt file.arrayBuffer() gefolgt von Uint8Array.toBase64() (auf Runtimes, die es haben) das Präfix komplett 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 Abschnitt 5 von RFC 4648, base64url, tauscht die beiden Sonderzeichen gegen - und _ aus und lässt das Padding weg, 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 Ihrer geistigen Werkzeugkiste.
Der Buffer von Node spricht diesen Dialekt seit Version 15.7.0, und die Kodierungs-Seite ist ein 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 + ist zu einem - geworden, das / zu einem _, und das Padding ist verschwunden, denn der base64url-Modus lässt es von Design her weg. Und die IETF ist explizit, dass das eine andere Kodierung ist, nicht dieselbe in einem Kostüm, also wenn eine Spezifikation "base64url" sagt, sollten Sie base64url erzeugen, nicht klassisches Base64 mit Suchen-und-Ersetzen. Die ES2026-Methode macht dieselben Wahlentscheidungen 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 Padding-Zeichen
Auf Runtimes ohne beides ist die Umwandlung ein Zwei-Zeichen-Tausch plus ein Padding-Abtrennen, und es ist eines der am meisten kopierten Snippets in 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 nie auf einen Prozent-Dekodierer treffen wird. Sie zu verwechseln ist der häufigste Interop-Bug in diesem ganzen Format.
Anmeldedaten versiegeln: Basic Auth, JWTs und PKCE
Drei Authentifizierungs-Ecken des Webs sind auf Base64 aufgebaut, und alle drei lassen sich einmal günstig von Hand bauen, was gut ist, denn zu wissen, was unter dem Paket passiert, ist es, was Sie ruhig hält, wenn das Paket Sie überrascht.
Erstens die HTTP-Basic-Authentifizierung (RFC 7617): Der Client sendet das Schemawort Basic plus die Base64-Kodierung von user-id:password. Eine Zeile, und eine ernste Warnung im Anhang:
const { Buffer } = require('node:buffer');
console.log('Basic ' + Buffer.from('octo:cat').toString('base64')); // "Basic b2N0bzpjYXQ="
Base64 ist hier Verschleierung, keine Sicherheit. Jeder, der den Header lesen kann, kann das Passwort lesen, also ist dieses Verfahren 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 gewöhnlichem JSON, und das dritte ist die Signatur. Ein HMAC-SHA256-Token von 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 padding-freies base64url
Beachten Sie die Details, die ein Token machen oder brechen: nirgends Padding (RFC 7515 lässt es weg), die Signatur wird über den wörtlichen String header + '.' + payload berechnet, nicht über die geparsten Objekte, und das Ganze ist nur so geheim wie der Schlüssel. In Produktion werden Sie ein Paket 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 erlaubt, sich sicher anzumelden: Der Client erzeugt einen hochentropischen code_verifier, veröffentlicht BASE64URL(SHA256(verifier)) als Challenge und beweist den Besitz des Verifiers beim Token-Tausch. 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 43-128-Bereichs
Altes Post braucht Umbruch: 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, umbricht sein Base64 bei 76 Zeichen pro Zeile und verlangt, dass die Zeilen mit CRLF enden, ein Relikt der 8-bit-clean-SMTP-Tagen, als sehr lange Zeilen echte Mail-Server zum Absturz brachten. RFC 7468, der die PEM-Regeln für Zertifikate und Schlüssel aufschreibt, ist noch strenger: Erzeuger müssen bei exakt 64 Zeichen pro Zeile umbrechen, die letzte Zeile kürzer, eingerahmt von -----BEGIN und -----END-Rüstungszeilen, die den Inhalt benennen.
Der Umbruch selbst ist eine Zeile, und die Rüstung ist 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-Werkzeuge, Java-Key-Stores) werden einen 4000-Zeichen-einzeiligen Base64-Blob ablehnen. Wenn Sie es konsumieren, müssen Sie das in der Regel nicht, denn der Dekodierer von Node überspringt Leerzeichen, also erledigt Buffer.from die Zeilenumbrüche für Sie - aber die Rüstungszeilen selbst sind kein Base64, also streichen 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 sie bedeutet nicht, dass Sie den Schritt, die Rüstung zu entfernen, überspringen können, wenn das Base64 irgendwo hingeht, das nichts überspringt, wie ein DER-Parser.
Wo kodierte Daten leben: Env, Config und Datenbanken
Base64 ist auch ein Speicherformat, und das ist gleichzeitig bequem und gefährlich leicht mit Sicherheit zu verwechseln. Umgebungsvariablen sind der klassische Wohnsitz: mehrere Secret-Manager und CI-Systeme reichen Ihnen Base64-kodierte Werte, und das Dekodieren ist eine Zeile:
const { Buffer } = require('node:buffer');
const stored = process.env.API_KEY_B64; // "c3VwZXItc2VjcmV0"
console.log(Buffer.from(stored, 'base64').toString('utf8')); // "super-secret"
Sagen Sie den wichtigen Satz laut: Kodierung ist keine Verschlüsselung. Ein Base64-"Geheimnis" in einer Umgebungsvariablen, einer .env-Datei oder einem Kubernetes-Secret (k8s speichert seine Secrets als Base64 im API und in etcd, und die Doku wiederholt es ständig) ist für jeden lesbar, der die Prozess-Umgebung, die Datei oder das Cluster lesen kann. Verwenden Sie dort Base64, weil der Transport (Shell, YAML, JSON) nur Text erlaubt, niemals, weil Sie glauben, es verberge etwas.
In Datenbanken ist Base64 die Standardbrücke für Binäres 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 bedanken, wenn jemand fragt, was die Bytes sind. Wenn Ihre Datenbank einen nativen Binär-Typ hat (Postgres bytea ist das Referenzbeispiel), bevorzugen Sie ihn: Die Bytes kosten nichts extra, und Sie sparen die 33-Prozent-Steuer für immer.
Streams kodieren, ohne Gruppen zu teilen
Base64 arbeitet in Drei-Byte-Gruppen, also muss ein Encoder, der beliebige Chunks empfängt, seinen Rest tragen: 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. Rechnen Sie pro Chunk nach und emittieren Sie nur vollständige Gruppen, und die Ausgabe ist byte-identisch mit dem Kodieren des ganzen 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 zu einem großen toString('base64')
});
encoder.end(Buffer.from('hello world, this is a stream!'));
Der flush-Callback ist das Detail, das jeder vergisst: die letzten ein oder zwei Bytes, die in einem normalen Chunk nie einen Partner fanden, bekommen ihr Padding und werden am Ende herausgeschoben. Dasselbe Weitertragen-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 Gruppen-Grenzen 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 an Speicher, bevor Ihr Buffer eintrifft. Die Obergrenze ist in Node explizit: buffer.constants.MAX_STRING_LENGTH sind 536870888 Zeichen, knapp unter 512 MiB an Text, das dekodiert zu etwa 400 MB an Bytes. Darüber hinaus ist ein einzelner String keine Option, und Streaming ist das einzige Spiel im Ort:
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-Encoder aus dem vorherigen Abschnitt, abgeflacht: in 64-KiB-Chunks lesen, den 1-bis-2-Byte-Rest tragen, vollständige Gruppen emittieren, den Schwanz flushen. Der Speicher-Fußabdruck bleibt bei etwa einem Chunk plus einem Rest, egal wie schwer die Datei ist. Und wenn die Empfänger-Seite Binäres annehmen kann, fragen Sie sich, warum Sie die Steuer überhaupt zahlen.
Einzeiler für das Terminal
Node verdoppelt sich als Base64-Encoder für die Kommandozeile, was praktisch ist, wenn Sie einen Konfigurations-Wert verpacken, eine API debuggen oder eine kleine Datei zwischen Maschinen durch eine Chat-Nachricht bewegen:
# 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 weggelassen
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64url"))' notes.txt
# Vom 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 ein eigenes abschließendes Zeilenende hinzu, was die Ausgabe für Copy-Paste und für $(...)-Substitution in Shell-Skripten sauber hält. Wenn Sie eine schön formatierte Datei mit umgebrochenen Zeilen wollen, leiten Sie das Ergebnis in Ihren Lieblings-Editor, oder fügen Sie am Ende des Einzeilers ein \n hinzu.
Fallen, die Entwicklern Stunden gekostet haben
Jede dieser Fallen hat in einer echten Codebase einen echten Nachmittag gekostet:
- Die Unicode-Wand:
btoa('héllo ⛳')wirftInvalidCharacterError, weil die Golfflagge nicht in ein Byte passt. Der Fix ist die UTF-8-Brücke: ErstTextEncoderzu Bytes, dann kodieren. In Node.js umgehen Sie das Problem komplett mitBuffer.from(text), das von UTF-8 ausgeht. - Das Legacy-Idiom: alter Code voller
btoa(unescape(encodeURIComponent(x)))funktioniert, aberescapeundunescapesind deprecatete Legacy-Funktionen. Wenn Sie diesen Code refaktorisieren, ersetzen Sie ihn mit derTextEncoder-Brücke, und das Verhalten bleibt identisch. - Das fehlende Kodierungs-Argument, umgekehrt: die Dekodierungs-Seiten-Falle ist
Buffer.from(str)ohne den 'base64'-Modus; der Encoder-Seiten-Zwilling ist die Annahme,Buffer.from(someString)tue irgendetwas 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 gewollt war. Seien Sie in beiden Richtungen explizit. - Die Padding-Ungleichung: JWTs und die meisten Token-Standards wollen base64url ohne Padding, MIME will klassisches Base64 mit Padding, und die beiden sind leicht zu kreuzen. Ein gepaddertes
=innerhalb eines JWT-Teils bricht strenge Verifizierer; ein fehlendes Padding, wo die Länge unbekannt ist, bricht nachsichtige Dekodierer. Passen Sie den Standard an, nicht Ihre Gewohnheit. - Der nicht-kanonische Schwanz: RFC 4648 verlangt, dass die ungenutzten Padding-Bits der letzten Gruppe null sind. Alle eingebauten Encoder erzeugen kanonische Ausgabe, aber ein von Hand gebastelter Encoder, der Bits per Hand verschiebt, kann Müll in diesen Bits hinterlassen, und ein strenger Dekodierer wird Ihr Payload ohne ersichtlichen Grund ablehnen. Wenn Sie Ihren eigenen Encoder schreiben, testen Sie gegen die Testvektoren von RFC 4648, nicht nur gegen Ihre eigenen Daten.
- Der vergessene Umbruch: MIME will 76-Zeichen-Zeilen und PEM will 64, und strenge Konsumenten (Mail-Gateways, Java-Key-Werkzeuge) lehnen einen einzeiligen Blob ab. Die Umkehrung ist seltener, aber real: einige Parser sind zeilenorientiert, und ein fehlendes CRLF am Ende einer PEM-Datei hat mehr Builds gebrochen als irgendein Bug im Base64 selbst.
- Die Sicherheits-Illusion: Base64 in einer Umgebungsvariablen, einer
.env-Datei oder einem Kubernetes-Secret ist keine Verschlüsselung. Es dekodiert mit einer Zeile Code, in jeder Sprache, für jeden, der die Datei oder das Cluster lesen kann. Behandeln Sie es als Transport-Kostüm, und lassen Sie die echten Kontrollen (Berechtigungen, TLS, Schlüssel-Rotation) den Schutz erledigen. - Die JSON-Verschwendung: 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 Dateigrößige über HTTP ist
multipart/form-dataoder ein roher binärer Body der bessere Transport, und Base64 ist für den Fall, dass der Kanal nur Text erlaubt. - Die Heap-Rechnung: ein kodierte String ist im JavaScript-Heap UTF-16, zwei Bytes pro Zeichen, und der dekodierte oder Quell-Buffer ist eine zweite Kopie der Daten. Eine 100-MB-Datei bedeutet kurz etwa 270 MB an String plus 100 MB an Buffer. Streamen Sie die großen, und halten Sie die kodierte Form so kurz referenziert, wie der Code es zulässt.
- 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 völlig brauchbares 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 packen
Die Browser-Seite ist eine lange, ruhige Geschichte. btoa() wurde Anfang 2011 im HTML5-Entwurf spezifiziert, und es sitzt seit Mitte der 2000er in jedem großen Browser, verhaltenstechnisch unverändert, mit seinem Ein-Byte-pro-Zeichen-Vertrag und seiner immer gepadderten Ausgabe. Dieser Vertrag ist älter als Typed Arrays - Binärstrings waren vor 2009 der einzige Weg, Bytes zu tragen -, deshalb denkt btoa() immer noch in "Binärstrings". Die moderne Hälfte der Geschichte ist sehr jung: Der TC39-Vorschlag, der nativen Base64 zu den Typed Arrays hinzufügte (zusammen mit Hex), wurde als Teil von ES2026 standardisiert, und er 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 auf einem anderen Takt. Die Buffer-Klasse wurde in Version 0.1.103, im Sommer 2010, zu einem Global, knapp fünf Jahre vor Node 1.0, und toString('base64') war über ein Jahrzehnt lang der Encoder der Wahl, mit den Alphabet-Idiosynkrasien jener Ära (es 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 Paket base64-js (drei Funktionen, null Abhängigkeiten) hat das Ökosystem jahrelang still als transitive Abhängigkeit getragen.
Das Format, dem sie dienen, ist älter als all das. Das Alphabet wurde 1987 zuerst für Privacy-Enhanced Mail standardisiert (RFC 989), die Revision von 1993 (RFC 1421) behielt es, und MIME übernahm es Monate später im selben Jahr mit seinem 76-Zeichen-Umbruch; RFC 3548 vereinigte die Base-N-Familie 2003 und fügte die URL-sichere Variante hinzu, die RFC 4648 im Jahr 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 den PKCE-Flow von OAuth. Die Encoder in diesem Artikel sind die letzte Meile eines Formats, das dreißig Jahre alt ist und immer noch Passagiere gewinnt.
Wert zu wissen, auf einer Party
btoa('GIF89a')liefert"R0lGODlh", den gesamten Magie-Header eines GIF in acht Zeichen. Es ist das kleinste "Hallo", das eine Binärdatei 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 paddet bedingungslos. Zwei Jahrzehnte dasselbe Alphabet, und die neuere API kann eine Sache, zu der die ältere nie berechtigt war.- Ein Alphabet, zwei offizielle Zeilenlängen: MIME umbricht bei 76, 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 RFC-Ära-E-Mails fügten grob weitere 3,5 Prozent für die Zeilenumbrüche hinzu. Ihr "kleiner" Konfigurations-String ist 37 Prozent fetter, ohne jeden Grund.
- Das kleine Paket
base64-jszieht auf npm über 100 Millionen Downloads pro Woche, fast alles davon versteckt in den Abhängigkeits-Bäumen anderer Pakete. Base64 ist der am meisten geschmuggelte Code im JavaScript-Ökosystem. - Kleine Buffer werden nicht einzeln zugewiesen: Node schnitzt sie aus einem gemeinsamen Pool von 65536 Bytes (
Buffer.poolSize), deshalb ist die Buffer-Erzeugung schnell, und deshalb existieren die "unsafe"-Zuteilungs-Varianten für die Fälle, in denen die Daten des Vormieters egal sind. - Der RFC, der 1998 Data-URLs definierte, warnt, sie seien "nur für kurze Werte nützlich", unter Berufung auf die 1024-Zeichen-Grenze von HTML-Attributen. Moderne Browser betten Megabyte-große Bilder als Data-URLs in dieselben Attribute ein, was entweder Fortschritt oder Größenwahn ist, je nach Ihrem Hero-Bild.
- Unix-Passwort-Hashes verwenden ihre eigenen Base64-artigen 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 in./A-Za-z0-9mischen. Es ist eine gute Erinnerung daran, dass "Base64" in einem Sicherheits-Kontext eine Familie ist, kein einzelnes Format. - Der Dekodierer von Node akzeptiert
-,_,+und/in beiden Modi,'base64'und'base64url', vier Zeichen, eine Tabelle. Der Encoder spricht, selbstverständlich, nur den Dialekt, um den Sie ihn baten.
Die Hälfte einer Rundreise
Base64 in JavaScript und Node.js zu kodieren kommt auf drei Entscheidungen an: Welche Bytes halten Sie in der Hand (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 das, und die Eingebauten 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 bekamen.
Und jedes Paket, das Sie hier versiegeln, wird eines Tages jemand anderes öffnen. Die Dekodierungs-Seite hat ihre eigene Schar von Fallen: der nachsichtige Dekodierer, der Müll ohne Ton schluckt, das Binärstring-Kostüm, das atob() Ihnen reicht, die Zeichensatz-Entscheidungen, die auf der Lese-Seite der Wand passieren, und die Stream-Logik, die das Weitertragen-Muster spiegelt, das Sie gerade gelernt haben. Diese Geschichte, mit Code-Beispielen für jeden Schritt, wird ausführlich in dem verwandten Base64-Dekodierungs-Artikel auf unserer Schwestersite behandelt. Lesen Sie ihn als Nächstes, denn die Fallen auf der anderen Seite des Alphabets sind leiser, und leiser ist genau so, wie sie gewinnen.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Dekodierung in JavaScript/Node.js: Ein vollständiger Leitfaden