Base64-Kodierung in JavaScript/Browser: Ein vollständiger Leitfaden
Sie haben etwas, das unterwegs sein muss, und die Straße ist nur breit genug für nacktes ASCII. Es könnte ein Bild sein, das in eine JSON-Antwort gehört, ein Konfigurations-Objekt, das mit in einer URL reisen muss, ein Token, dessen drei Segmente aus Punkten und Buchstaben bestehen, eine Datei, die eine API partout als Base64-String in einem JSON-Body haben will. Base64 ist die Mautstelle genau für diese Situation, und die Startseite dieser Site erklärt das Format bereits Schritt für Schritt - vier druckbare Zeichen als Stellvertreter für jeweils drei Bytes, mit =-Padding, um die Gruppe abzuschließen. Deshalb ist hier die eine Zahl, die Sie beim Lesen im Kopf behalten sollten: Kodieren ist die wachsende Richtung. Drei Bytes, die Sie abliefern, kommen als vier Zeichen zurück, eine Größensteuer von etwa 33 Prozent, eingetrieben über Bandbreite, persistenten Speicher und Hauptspeicher. Verwenden Sie Base64, wenn der Kanal druckbaren Text verlangt, und wissen Sie genau, was diese Steuer Sie kostet.
Die ermutigende Nachricht: Der Browser konnte diese Aufgabe schon immer ohne ein einziges Paket erledigen. btoa() ist seit Anfang der 2000er dabei, TextEncoder verwandelte Ihren echten Unicode-Text vor einem Jahrzehnt in ehrliche Bytes, und in der Baseline-2025-Welle bekam die Plattform endlich Uint8Array.toBase64(), das Byte-Arrays direkt kodiert, mit einer Option für das URL-sichere Alphabet. Dieser Artikel ist die Entscheidungs-Karte: welches Werkzeug für welche Aufgabe, wo die scharfen Kanten lauern (sie alle führen zurück auf dieselbe Grenze) und die konkreten Rezepte für die Stellen, an denen Sie tatsächlich Base64 produzieren sollen.
Den richtigen Encoder wählen
Es gibt nicht mehr den einen "den" Encoder, und zum falschen zu greifen ist, wie die klassischen Bugs geboren werden. Die Tabelle unten ist der komplette Entscheidungs-Baum:
| Situation | Greifen Sie zu |
|---|---|
| Einfacher ASCII-Text, Einmalwert | btoa(text) |
| Echter Text mit Umlauten, Emoji, CJK | new TextEncoder().encode(text), dann btoa oder toBase64 |
Bytes schon in einer Uint8Array |
bytes.toBase64() in Browsern von 2025 und neuer, sonst die chunk-weise btoa-Brücke |
| URLs, JWTs, Dateinamen | toBase64({ alphabet: 'base64url', omitPadding: true }) |
| Alte Browser oder eine geteilte Codebase | js-base64, oder das klassische TextEncoder + btoa-Rezept |
Das Muster unter der Tabelle: btoa() liest nur Ein-Byte-Zeichen, also muss alles, was kein ASCII ist, erst zu einem Byte-Array werden, und genau dieses Byte-Array ist es, um das die modernen APIs gebaut wurden. Behalten Sie "Text wird zu Bytes, Bytes werden zu Base64" im Kopf, und jedes Rezept in diesem Artikel ist dieselbe Sache: zwei Schritte, nur mit anderen Namen.
btoa und die Latin1-Grenze
btoa(stringToEncode) - binärer String zu ASCII-String - ist der ursprüngliche Encoder, verfügbar in jedem Browser, der zählt (Chrome 4, Firefox 1, Safari 3, IE 10 und neuer, alle Worker-Bereiche, und Node seit Version 16). Sein Vertrag hat eine Klausel, und an genau dieser Klausel geht alles schief: Jedes Zeichen in der Eingabe muss einen Codepunkt zwischen 0 und 255 haben. Die Funktion liest Codepunkte, keine UTF-8-Bytes, also segelt "é" (Codepunkt 233) vorbei, während "你" (Codepunkt 20320) eine DOMException namens InvalidCharacterError wirft, bevor ein einziges Zeichen kodiert ist. Die Grenze ist nicht "ASCII", sie ist nicht "Unicode", sie ist exakt 256, und sie enthält die Steuerzeichen ganz unten - ein NUL-Byte zu kodieren ist legal und sinnvoll, und das ist einer der Gründe, warum die Funktion überhaupt existiert.
Das vollständige Verhalten, Zeile für Zeile:
| Eingabe | Ergebnis |
|---|---|
"Hello, World!" |
"SGVsbG8sIFdvcmxkIQ==" - der Lehrbuchfall |
"" (leerer String) |
"" - nichts rein, nichts raus |
"\u0000" (NUL) |
"AA==" - Steuerzeichen sind Bürger erster Klasse |
"a\u00e9z" (é, Codepunkt 233) |
"Yel6" - der gesamte Latin1-Bereich geht durch |
"\u0100" (Codepunkt 256) |
wirft InvalidCharacterError - ein Schritt über die Grenze |
"h\u4f60" (你, Codepunkt 20320) |
wirft InvalidCharacterError - und dasselbe gilt für jedes Emoji, denn sie alle liegen weit über 255 |
Zwei praktische Hinweise. Die Fehlermeldung unterscheidet sich je nach Engine - Firefox sagt "String contains an invalid character", Chrome sagt, der String "contains characters outside of the Latin1 range" - also fangen Sie in jedem defensiven Code anhand des Ausnahme-Namens ab. Und geworfen wird beim ersten auffälligen Zeichen, nicht am Ende: btoa kodiert nicht den halben String und entschuldigt sich. Wenn Sie das Latin1-Verhalten aber absichtlich wollen (ein Byte-String kodieren, der absichtlich aus Codepunkten 0-255 gebaut wurde), macht die Funktion genau das, was Sie gebeten haben, und die Tabelle oben ist ihre gesamte Persönlichkeit.
Die Bytes-Brücke
Die Frage lautet also: Wie kommen echte Daten - die UTF-8-Bytes Ihres Textes, der Inhalt einer Datei, die Ausgabe eines Canvas - in die Eingabe von btoa()? Die Antwort ist die "Bytes-Brücke": ein JavaScript-String, in dem jedes Zeichen einen Byte-Wert hält, derselbe Trick, den die Dekodierer produzieren und den btoa nativ versteht. Die naive Version ist eine Schleife:
function bytesToBase64 (bytes) {
let binary = '';
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
Korrekt, aber String-Konkatenierung in einer Schleife ist für große Dateien langsam, und der beliebte Shortcut - String.fromCharCode.apply(null, bytes), der das ganze Array in einem Aufruf als Argumente füttert - hat eine harte Kante. Funktionsaufrufe haben ein Limit für die Anzahl der Argumente, und es wird weit vor Ihrem ersten Megabyte erreicht:
const big = new Uint8Array(1000000);
btoa(String.fromCharCode.apply(null, big));
// RangeError in Firefox: "too many arguments provided for a function call"
// RangeError in Chrome: "Maximum call stack size exceeded"
Der Fix, der mehr Datei-Upload-Funktionen gerettet hat als jede andere Einzeländerung: die Brücke in Chunks überqueren, ein paar tausend Zeichen auf einmal, und die Ergebnisse verbinden:
function bytesToBase64Chunked (bytes) {
const CHUNK = 0x8000;
const parts = [];
for (let i = 0; i < bytes.length; i += CHUNK) {
parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK)));
}
return btoa(parts.join(''));
}
Jeder Chunk ist klein genug, um sicher angewendet zu werden, subarray gibt eine Ansicht ohne Kopieren, und das Verbinden produziert exakt denselben Binärstring, den die Schleife produziert hätte. Jetzt die Text-Seite der Münze. Für jeden echten Text verwandelt TextEncoder - der UTF-8-Encoder der Plattform, verfügbar in Firefox 18, Chrome 38, Safari 10.1 und überall seither - Ihren String in ehrliche Bytes, bevor die Brücke ihre Arbeit tut:
const bytes = new TextEncoder().encode('hello 你好');
const base64 = bytesToBase64Chunked(bytes);
console.log(base64); // "aGVsbG8g5L2g5aW9"
Diese Ausgabe ist das, was "hello 你好" auf dem Draht wirklich ist: sechs ASCII-Bytes plus sechs UTF-8-Bytes für die zwei chinesischen Zeichen, alle in derselben druckbaren Verkleidung. Wenn Ihr Text nicht UTF-8 ist - und auf dem Web ist es das meistens - brauchen Sie zuerst den anderen Zeichensatz. Das bedeutet, den Text irgendwo zu kodieren, der diesen Zeichensatz spricht, in der Regel der Server. TextEncoder weigert sich bewusst, zu raten, und das ist richtig so.
Der 2025er-Shortcut: Uint8Array.toBase64
Wenn Sie bereits eine Uint8Array in der Hand haben, ist die Brücke ein Umweg, denn das neue ECMAScript- (ES2026-) Feature kodiert das Array direkt: bytes.toBase64(options). Es ist in Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 und Deno 2.5 gelandet - dieselbe Baseline-2025-Welle wie sein Dekodier-Bruder - und es nimmt zwei Optionen an, die aus ihm den vielseitigsten Encoder der Plattform machen. Die erste ist alphabet: "base64" (der Standard) oder "base64url". Die zweite ist omitPadding: Setzen Sie es auf true, und die =-Zeichen am Ende werden weggelassen, was die Form ist, die die meisten URL-freundlichen Empfänger wollen. Alles andere als Optionen zu übergeben wirft eine TypeError, was die API höflich Ihren Tippfehler benennt:
const bytes = new Uint8Array([251, 255]);
console.log(bytes.toBase64()); // "+/8="
console.log(bytes.toBase64({ omitPadding: true })); // "+/8"
console.log(bytes.toBase64({ alphabet: 'base64url' })); // "-_8="
Diese zwei Bytes sind so gewählt, dass sie dem Alphabet maximal grob begegnen: Sie erzeugen im Standardmodus ein + und ein /, also zeigt die letzte Zeile exakt, was sich ändert, wenn Sie zu base64url wechseln. Performance ist der stille Bonus: Auf einem aktuellen Firefox dauert das Kodieren von zehn Megabyte mit toBase64 etwa fünf Millisekunden, während der String-Brücken-Weg oben etwa fünfzehnmal so lange braucht, weil er dabei einen riesigen Zwischen-String aufbaut. In älteren Browsern bleibt die Brücke für alles unter ein paar Megabyte völlig brauchbar - und die chunk-weise Version oben ist die, die Sie wollen, aus den Gründen im letzten Abschnitt.
URL-sichere Ausgabe
Base64 hat eine dedizierte Variante für die Stellen, an denen +, / und = Schaden anrichten, und sie verdient einen eigenen Abschnitt, denn so viel kaputter Code ist nur Standard-Base64, das auf eine URL getroffen ist. In einem Query-String ist + ein Leerzeichen; in einem Pfad ist / ein Trenner; und = will an manchen Stellen prozent-kodiert werden. Das URL- und Dateinamen-sichere Alphabet aus RFC 4648, Abschnitt 5 - base64url - tauscht diese zwei Zeichen gegen - und _ aus, und da die Datengröße auf der empfangenden Seite meist bekannt ist, erlaubt es auch, das Padding komplett wegzulassen. Die Ausgabe reist durch URLSearchParams, Pfadsegmente, Fragmente und Dateinamen ohne ein einziges Prozentzeichen.
Mit der 2025er-API ist das ein Options-Objekt:
const params = new URLSearchParams();
params.set('payload', bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(params.toString()); // "payload=-_8" - überhaupt keine prozent-Kodierung
In älteren Browsern konvertieren Sie nach dem Kodieren mit btoa. Zwei Replaces und ein Trim erledigen die ganze Arbeit:
function toUrlBase64 (base64) {
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
console.log(toUrlBase64(btoa('hi?/x'))); // "aGk_L3g"
Drei Regeln halten den Kanal sauber. Wählen Sie ein Alphabet pro Kanal und bleiben Sie dabei - ein Wert, der + und - mischt, gehört zu keiner der beiden Familien, und kein Dekodierer wird erraten, welches Sie meinten. Padding ist ein Vertrag, kein Vorschlag: Wenn Sie es weglassen, muss der Empfänger für einen ungepadderten Wert bereit sein, und wenn Sie es behalten, darf der Empfänger daran nicht ersticken (Browser sind nachsichtig, manche JSON-Schemata nicht). Und denken Sie daran, dass der Tausch umkehrbar und verlustfrei ist - - und _ mappen auf dieselben 62. und 63. Alphabet-Positionen, die + und / besetzen, also geht nichts verloren, wenn Sie das freundlichere Paar wählen.
Bilder auf Reisen schicken: Data-URLs
Die älteste und sichtbarste Anwendung von Base64 im Browser ist die Data-URL: data:, ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma, dann der Payload. Text-Payloads sind prozent-kodiert; binäre Payloads - Bilder, Schriften, Audio - sind Base64, und der Browser rendert sie mit null HTTP-Anfragen. Für eine Bilddatei, die der Nutzer gerade gewählt hat, macht der FileReader das Kodieren für Sie und reicht die fertige URL zurück:
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result); // "data:image/png;base64,iVBORw0KGgo..."
imageElement.src = reader.result;
};
reader.readAsDataURL(file);
Das Ergebnis ist ein fertiges src, ein Wert, den Sie in localStorage speichern oder in einem JSON-Body senden können. Wenn das Bild stattdessen auf einem Canvas liegt - ein Screenshot, ein verarbeitetes Foto, ein generiertes Diagramm - erledigt canvas.toDataURL() diese Aufgabe seit den allerersten Browser-Releases, und es lässt Sie sogar das Format und, bei verlustbehafteten Formaten, die Qualität wählen:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(photo, 0, 0);
const pngUrl = canvas.toDataURL('image/png');
const jpegUrl = canvas.toDataURL('image/jpeg', 0.8);
Drei Fallen, mit denen Sie planen sollten. Erstens: die tainted-canvas-Regel. Wenn Sie ein cross-origin Bild ohne CORS-Erlaubnis auf das Canvas gezeichnet haben, wirft jeder Versuch, die Pixel zurückzulesen - inklusive toDataURL - eine SecurityError. Der Fix: Laden Sie das Bild mit crossOrigin = 'anonymous' und stellen Sie sicher, dass der Server die richtigen Header sendet. Zweitens: Der Quality-Parameter wird für PNG ignoriert und bedeutet nur etwas für JPEG (und WebP) - eine häufige Quelle für "Warum ist mein PNG größer". Drittens, und das größte: Der Payload ist etwa 33 Prozent größer als die Datei, und er liegt als String in der Seite. Für Bilder, die nie den Browser verlassen, gibt es eine kostenlose Alternative - eine Objekt-URL, die den Blob einpackt, ohne ihn überhaupt zu kodieren:
const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
URL.revokeObjectURL(objectUrl); // wenn Sie fertig damit sind
Die Arbeitsteilung, die sich daraus ergibt: Objekt-URLs für alles, was auf der Seite bleibt, Data-URLs für alles, was kopiert, gespeichert oder als Text gesendet werden muss. Beides ist erste Klasse; sie lösen nur unterschiedliche Probleme.
Ein JWT bauen und signieren
Wenn Sie Tokens im Browser generieren - für einen selbst gehosteten Auth-Flow, eine Demo oder ein serverloses Frontend - ist das kompakte JWS-Format drei base64url-Segmente: header, payload, signature, nirgends Padding. Die Web Crypto API kümmert sich um die Signatur; das Kodieren ist exakt die URL-sichere Ausgabe aus zwei Abschnitten zuvor:
const encoder = new TextEncoder();
const segment = (bytes) =>
bytes.toBase64({ alphabet: 'base64url', omitPadding: true });
const header = segment(encoder.encode(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
const payload = segment(encoder.encode(JSON.stringify({ sub: '1234567890', name: 'John Doe' })));
const key = await crypto.subtle.importKey(
'raw',
encoder.encode('shared-secret'),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = segment(
new Uint8Array(
await crypto.subtle.sign('HMAC', key, encoder.encode(header + '.' + payload))
)
);
const token = header + '.' + payload + '.' + signature;
Zwei Details sind wichtiger als die Interna. Die Signatur deckt exakt header + '.' + payload ab - die rohen Segmente, nicht das JSON - also macht jede Änderung an einem der beiden Teile das Token ungültig, was der ganze Punkt ist. Und crypto.subtle.sign gibt ein rohes ArrayBuffer zurück, daher das Ein-Zeilen-Einpacken in eine Uint8Array vor dem Segment-Encoder. Für RSA-basierte Tokens ist der Flow identisch mit RS256 und einem Schlüsselpaar, und wenn Sie einen öffentlichen Schlüssel als JWK exportieren (crypto.subtle.exportKey('jwk', key)), kommen die numerischen Mitglieder - n, e und bei privaten Schlüsseln d, p, q - automatisch als ungepaddertes base64url heraus. Die Sicherheits-Hinweise sind dieselben wie bei jedem Token: Ein alg: "none"-Header ist eine Bitte, die Verifikation zu überspringen, Zeit-Claims (exp, nbf) müssen durchgesetzt werden, und ein Server, der für dasselbe Publikum sowohl HMAC als auch RSA akzeptiert, öffnet die klassische Key-Confusion-Tür. Richtig kodieren, richtig signieren, auf der empfangenden Seite verifizieren.
Authentifizierungs-Header
Das einfachste Authentifizierungs-Schema im Web ist auch das lehrreichste dafür, was Base64 ist und was nicht. HTTP Basic sendet Authorization: Basic gefolgt vom Base64 von username:password - ein Aufruf, keine Bytes-Brücke nötig, denn Benutzernamen und Passwörter sind (hoffentlich) Klartext:
const credentials = btoa('alice:secret123');
fetch('/api/me', {
headers: { Authorization: 'Basic ' + credentials }
});
// Authorization: Basic YWxpY2U6c2VjcmV0MTIz
Und hier ist die Lektion, die auf eine Zeile passt: Base64 ist keine Verschlüsselung. Der obige Header ist einen atob-Aufruf von alice:secret123 entfernt - für den Angreifer und für jeden, der die Logs liest - also ist Basic-Auth nur über HTTPS akzeptabel, wo der Transport der eigentliche Schutz ist und Base64 nur das Format. Für alles, das länger lebt als eine Anfrage, bevorzugen Sie Token-basierte Schemata: Ein Bearer-Token ist auch ein einzelner Header, aber es ist ein zufälliger Wert, dessen Geheimnis nie im Header mitgeführt werden muss, und er kann widerrufen werden. Die Kodierungs-Wahl zwischen den beiden ist trivial - beide sind btoa oder Klartext - aber die Sicherheits-Wahl nicht, und sie sollte mit Absicht getroffen werden.
Dateien rein, Text raus
Uploads sind der Ort, an dem die 33-Prozent-Steuer in echtem Geld aufgerufen wird, denn die Datei ist meist das Größte auf der Seite. Es gibt zwei Wege, und der erste ist der, den Sie standardmäßig nehmen sollten: Multipart-Form-Daten. FormData trägt die Datei als Roh-Bytes in einem Standard-Body, der Browser macht das Framing, und Base64 ist nirgends im Spiel - keine Größensteuer, kein Zwischen-String, und die Bytes streamen zum Server, während sie gelesen werden:
const form = new FormData();
form.append('upload', file);
await fetch('/api/upload', { method: 'POST', body: form });
Der zweite Weg ist für die APIs, die auf einem JSON-Body mit der Datei als String bestehen - manche Serverless-Funktionen, manche Mobile-Backends, manche Legacy-Dienste. Dort ist das Kodieren eine Zeile pro Datei, und die Kosten sind genau das, was die Steuer verspricht: Eine 5-Megabyte-Datei wird zu einem 6,7-Megabyte-String, der dann in JSON serialisiert wird, der dann gesendet wird. In Ordnung für ein Foto, schmerzhaft für ein Video:
const bytes = new Uint8Array(await file.arrayBuffer());
const body = JSON.stringify({
name: file.name,
content: bytes.toBase64()
});
await fetch('/api/upload-json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
});
Für große Dateien auf diesem Weg: Bauen Sie nicht einen riesigen String in einem einzigen Aufruf - bauen Sie ihn in Scheiben, wobei jede Scheibe ein Vielfaches von drei Bytes ist. Diese Ausrichtung macht den Trick legal: Ein Vielfaches von drei Bytes kodiert zu einem sauberen Vielfachen von vier Zeichen ohne Padding, also konkatenieren unabhängig kodierte Scheiben zu exakt der Kodierung der gesamten Datei, und nur die letzte Scheibe trägt jemals Padding:
async function encodeLargeFile (file) {
const bytes = new Uint8Array(await file.arrayBuffer());
const SLICE = 3 * 1000 * 1000;
const parts = [];
for (let i = 0; i < bytes.length; i += SLICE) {
parts.push(bytes.subarray(i, i + SLICE).toBase64());
}
return parts.join('');
}
Die gleiche Ausrichtungs-Idee ist der Grund, warum Sie einen Base64-String nie an einem beliebigen Punkt trennen sollten und erwarten, dass die Stücke von allein dekodieren - eine Drei-Byte-Gruppe ist das Atom, und ein Schnitt mitten in einer hinterlässt ein hängendes Fragment. Downloads sind das Spiegelbild: Für eine generierte Datei kann eine kleine über eine Data-URL auf einem Download-Link rausgehen, aber für alles Substanzvolle ist ein Blob plus eine Objekt-URL der gesunde Weg, denn der Browser muss den ganzen Payload von Anfang an nie als String tragen.
Zustand speichern und teilen
Zwei weitere reine Text-Kanäle, in denen Base64 echte Arbeit leistet. Der erste ist der Speicher: localStorage und sessionStorage halten Strings, also werden strukturierte oder binäre Daten kodiert, bevor sie reingehen. Der Roundtrip ist ein Kodieren und ein Dekodieren, und es lohnt sich, beide Seiten zusammen zu sehen, denn ein Speicher-Bug ist fast immer ein Zeichensatz-Mismatch zwischen ihnen:
const state = { theme: 'dark', draft: 'hello' };
const packed = new TextEncoder().encode(JSON.stringify(state));
localStorage.setItem('app-state', new Uint8Array(packed).toBase64());
const raw = atob(localStorage.getItem('app-state'));
const bytes = Uint8Array.from(raw, (c) => c.codePointAt(0));
const state = JSON.parse(new TextDecoder().decode(bytes));
Budgetieren Sie das aber richtig: Der Origin bekommt etwa 5 Megabyte localStorage, Ihr gespeicherter String ist 33 Prozent dicker als die Daten, und solange die Seite offen ist, lebt der String außerdem als UTF-16 im Speicher - erneut doppelt so lang. Ein 3-Megabyte-Asset sind 4 Megabyte Speicher und 8 Megabyte Hauptspeicher, so wird aus einem "kleinen" Feature ein Quota-Fehler. Der zweite Kanal ist die URL selbst: Share-Links, Deep-Links und OAuth-Zustand wollen alle strukturierte Daten an einem Ort, der Copy-Paste übersteht. Das Rezept: kompakter Zustand, JSON, dann base64url ohne Padding, damit der Wert überhaupt keine prozent-Kodierung braucht - und halten Sie die ganze URL unter ein paar tausend Zeichen, denn ab dort werden ältere Clients, Proxys und Logging-Tools nervös.
E-Mail und MIME
Base64 ist älter als das Web, und ihr Heimatgrund ist E-Mail. MIME-Anhänge mit Content-Transfer-Encoding: base64 sind die Art, wie eine binäre Datei in einem Text-Protokoll mitfährt, und die Konvention, die aus dem alten 76-Zeichen-Zeilenlimit des Nachrichtenformats folgt, lohnt sich zu kennen: den kodierten Body umbrechen, 76 Zeichen pro Zeile. Der Browser kann kein SMTP senden, aber er tut zwei E-Mail-Jobs - MIME-Bodies bauen, die ein Backend-Relay sendet, und die Anhänge von empfangenen Nachrichten anzeigen - und beide berühren das Kodieren. Der Umbruch selbst ist eine Zwei-Zeilen-Funktion, und die Reihenfolge der Operationen zählt: erst kodieren, dann umbrechen, denn btoa wirft bei einem Zeilenumbruch in seiner Eingabe nicht - es kodiert den Bruch als Payload-Byte, und Ihre Zeilenumbrüche landen in der Ausgabe:
function wrapForMime (base64, width) {
const w = width || 76;
return base64.match(new RegExp('.{1,' + w + '}', 'g')).join('\r\n');
}
Die empfangende Seite ist die, die nichts kostet: atob überspringt ASCII-Leerzeichen als Teil seines Standardverhaltens, also dekodiert ein umgebrochener MIME-Body genau so, wie er ankam, Zeilenumbrüche inklusive, ohne einen Entpack-Schritt. Wenn Sie einen Webmail-Client oder einen Anhang-Picker bauen, ist diese eine Asymmetrie - der Encoder muss saubere Zeilen produzieren, den Dekodierer kümmert es nicht - die ganze MIME-Geschichte in einem Satz.
Wann Sie zu einer Bibliothek greifen sollten
Mit den nativen Werkzeugen oben ist eine Bibliothek selten nötig, und der ehrliche Rat lautet: Standardmäßig zur Plattform, und ein Paket nur dazunehmen, wenn eine echte Anforderung darauf zeigt. Die drei, die tatsächlich in Codebases auftauchen:
js-base64 (npm install js-base64) ist der Allrounder: Ein kleiner, reiner-JavaScript-Transcoder, der UTF-8-Strings als Bürger erster Klasse behandelt - Base64.encode auf einem CJK-String tanzt den UTF-8-Tanz für Sie - und, nützlich fürs Dekodieren genauso wie fürs Kodieren, in decode beide Alphabete akzeptiert und eine isValid-Prüfung mitliefert. Es ist die richtige Antwort, wenn Sie Browser anvisieren, in denen die 2025er-APIs fehlen, und ein einzelner Import Strings und Bytes abdecken soll:
import { Base64 } from 'js-base64';
const encoded = Base64.encode('小飼弾'); // "5bCP6aO85by+" - UTF-8 wird für Sie erledigt
const decoded = Base64.decode('5bCP6aO85by-'); // liest Standard und URL-sicher gleichermaßen
const valid = Base64.isValid(encoded); // true
base64-js ist der byte-orientierte: fromByteArray und toByteArray auf Uint8Arrays, keine Abhängigkeiten, das Arbeitstier des alten browserify-Ökosystems und immer noch eine gute Wahl, wenn Ihr Code in typed arrays lebt und Sie wollen, dass das Kodieren eine reine Funktion der Bytes ist. Und wenn Ihr Grund, eine Bibliothek zu wollen, "Ich mag die 2025er-API, aber ich kann keine 2025er-Browser verlangen" ist, lautet die Antwort gar nicht Base64-Paket, sondern Polyfill: core-js (und das Babel-Preset, das es mitzieht) implementiert Uint8Array.fromBase64 und die Freunde, so können Sie den Code im neuen Stil einmal schreiben und den Shim die Lücke in älteren Engines füllen lassen. Wählen Sie nach Einschränkung - alte Browser, String-Bequemlichkeit oder Byte-Reinheit - nicht nach Gewohnheit.
Fallen, die Entwicklern Stunden kosten
btoaauf einen String mit einem Zeichen über Codepunkt 255 aufzurufen. Es wirft, es verwüstet nicht, und es stoppt beim ersten Übeltäter. Der Fix ist immer derselbe: ZuerstTextEncoder, dann die Brücke.- Die
fromCharCode.apply-Kante bei großen Arrays. Eine Million Argumente ist in beiden großen Engines einRangeError. Die Brücke in Chunks aufteilen oder zutoBase64wechseln. - Die Größensteuer zu vergessen, wo sie am meisten sticht: im Speicher. Eine Datei in
localStorageist 33 Prozent größer als die Datei, und die Quote ist pro Origin und mit allem anderen geteilt, was Ihre App speichert. - Standard-Base64 trifft auf einen Query-String. Das
+kommt als Leerzeichen an, das/bricht den Pfad, und die Bug-Reports sagen "die API ist flaky". URL-sichere Ausgabe, kein Padding, und das ganze Bug-Genre verschwindet. - Inkonsistentes Padding über Dienste hinweg. Ein Gateway behält das
=, ein anderes streicht es, ein drittes fügt es wieder hinzu. Der Empfänger muss für beide Formen bereit sein, und der Vertrag sollte sagen, welche die offizielle ist. - Base64 als Schloss zu behandeln. Es ist ein Serialisierungsformat, ein Funktionsaufruf von Klartext entfernt, und "mit Base64 kodiert" in einem Security-Review ist ein Befund, keine Kontrolle.
- Binär-Strings als Speichermodell. Ein dekodiertes oder kodiertes Megabyte reist in UTF-16 mit zwei Megabyte; eine
Uint8Arrayhält es bei einem. Bei großen Payloads die Bytes von Anfang bis Ende in typed arrays halten. - Doppel-Kodierung. Ein Wert, der bereits Base64 war, wird nochmal kodiert, und der Konsument dekodiert einmal und bekommt einen String aus Buchstaben statt Daten. Bei Zweifeln: Prüfen, bevor Sie einwickeln - ein String, der bereits im Alphabet mit gültigem Padding ist, ist schon ein Smell.
- Einen JWT-Payload zu vertrauen, weil er sauber dekodiert hat. Dekodierbarkeit ist keine Echtheit. Verifizieren Sie die Signatur mit dem richtigen Schlüssel und dem richtigen Algorithmus, bevor Sie einen einzigen Claim lesen.
Performance: Was eine Million Bytes kostet
Base64 im Browser ist günstig, wo es teuer war, und das Budget hat jetzt drei Positionen statt einer. CPU: Auf einem aktuellen Firefox kodiert Uint8Array.toBase64 zehn Megabyte in etwa fünf Millisekunden, während die chunk-weise btoa-Brücke etwa fünfzehnmal so lange braucht - nicht, weil btoa langsam ist, sondern weil die Brücke unterwegs einen riesigen Zwischen-String aufbaut. Wenn Ihr Kodier-Budget in Millisekunden liegt, nutzen Sie die native Methode; wenn Sie ein 2-Kilobyte-Konfigurations-Objekt kodieren, liegen beide unter der Wahrnehmungsschwelle. Bandbreite: Das ist die permanente Steuer - jedes Byte, das Sie kodieren, kostet 1,33 Bytes auf dem Draht, plus das Framing, das der Transport hinzufügt. Messen Sie die Übertragung, bevor Sie das Kodieren "optimieren". Speicher: Der kodierte String ist die größte transiente Allokation, die Sie tätigen, und für eine 5-Megabyte-Datei ist es ein 6,7-Megabyte-String, oder etwa 13,4 Megabyte UTF-16-Speicher, solange die Seite ihn hält. Die praktischen Folgen ergeben sich aus der Arithmetik: Große Kodierungen in Scheiben schneiden, damit kein einzelner String riesig wird, die Zwischen-Bytes so bald wie möglich freigeben, sobald der String existiert, Objekt-URLs und Multipart bevorzugen, wenn die Bytes nie druckbar sein mussten, und Multi-Megabyte-Arbeit in einen Web Worker verschieben, wenn der Haupt-Thread weiter sanft scrollen muss. Das Format ist fast vier Jahrzehnte alt; die Plattform ist ihm endlich hinterhergekommen.
Wie Browser das Kodieren lernten
Der Encoder hat eine Geschichte, und sie erklärt die Relikte, die Sie erben werden. btoa - "binary to ASCII", der Name ist wörtlich, und atob sind einfach dieselben Wörter rückwärts - wurde 2011 in die HTML-Spezifikation geschrieben, reverse-engineered aus den Browsern, die es längst ausgeliefert hatten: Firefox seit 2004, Safari 3, Chrome 4. Internet Explorer übersprang, typisch für ihn, beide Funktionen bis Version 10 im Jahr 2012, und genau dieses eine Fehlen ist der Grund, warum ein Jahrzehnt JavaScript voller selbst gebastelter Base64-Tabellen und eines bestimmten Unicode-Spruchs ist: btoa(unescape(encodeURIComponent(str))). Es funktionierte - encodeURIComponent produziert prozent-escaped UTF-8, und unescape machte daraus einen Byte-String - aber es war auf unescape() gebaut, dasjenige im Paar, das die Sprache deprecatet hatte, und es überlebte jahrelang in Browser-Code aus purer Trägheit. Der prinzipienbasierte Fix kam mit dem Encoding-Standard: TextEncoder und TextDecoder, in Firefox 18 (2013), Chrome 38 (2014), Safari 10.1 (2017) und in keiner einzigen IE-Version - eine weitere IE-Lücke, ein weiteres Jahrzehnt an Workarounds. Node.js erzählt die serverseitige Hälfte der Geschichte: Es hatte Buffer mit Base64 vom ersten Tag an, aber atob und btoa als Globals erst ab Version 16 im Jahr 2021, davor trugen zwei kleine npm-Shims die Last. Und dann, quer über Ende 2024 und 2025, lieferte die Sprache selbst Base64 - Uint8Array.toBase64 und die Freunde in Firefox 133 (November 2024), Safari 18.2 (Dezember 2024), Chrome 140 (September 2025) und Node 25 (Oktober 2025) - und das Feature wurde als Baseline 2025 markiert - dasselbe Feature-Set, das die Plattform zwanzig Jahre lang mit Helfern approximiert hatte, jetzt Standard. Das lustige Trivia am Ende des Artikels dreht sich größtenteils darum, wie lange jedes Stück brauchte, um anzukommen.
Wussten Sie schon?
- Die Funktionsnamen sind ein Satz:
btoaist "binary to ASCII" undatobist "ASCII to binary". Die Richtung steckt im Namen, weshalb das Paar seit den 2000ern selbstdokumentierend ist. - Der am häufigsten kodierte String in der Geschichte der Informatik ist wahrscheinlich "hello":
btoa('hello')istaGVsbG8=, die Ausgabe jedes Tutorials, jeder Testsuite und jedes Interview-Whiteboards auf dem Planeten. - Jeder gültige Base64-String hat eine Länge, die ein Vielfaches von vier ist, Padding inklusive. Die
=-Zeichen sind ein Fingerabdruck: Eines davon heißt, die letzte Gruppe hielt zwei Bytes, zwei davon, sie hielt eins. - Der 76-Zeichen-Zeilenumbruch in MIME und in den meisten Kommandozeilen-Tools ist eine Erbschaft aus dem E-Mail-Zeitalter, als die Zeilenlänge des Nachrichtenformats das Limit setzte. Die Zahl hat drei Jahrzehnte, in denen alles immer schneller wurde, überlebt.
- "Data URI" ist ein zurückgedrängter Name. Der WHATWG hat es während der großen URI-zu-URL-Harmonisierung in "data URL" umbenannt, weshalb Spezifikationen, Blog-Posts und Paketnamen es alle im selben Absatz unterschiedlich schreiben.
btoa('')gibt''zurück: eine leere Eingabe produziert eine leere Ausgabe, kein Padding, kein Sonderfall - der einzige Base64-String mit null Zeichen (seine Länge, 0, ist immer noch ein Vielfaches von vier).- Ein Canvas kann ein Foto mit
toDataURLin eine Data-URL verwandeln - eine Fähigkeit, die seit IE 9, Firefox 2 und Safari 4 existiert und die meiste der Web-Plattform vorwegnimmt, die wir für "modern" halten - und es mit einem<img>-Tag und einemFileReaderzurückholen. - Der WebSocket-Handshake kodiert
SHA-1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)in Base64, und die GUID ist eine feste Konstante im RFC, die genau so gewählt wurde, damit kein einfacher HTTP-Server den Handshake je versehentlich abschließen kann.
Wohin von hier aus
Das ganze Handwerk des Kodierens im Browser passt auf eine Seite: btoa für die einfachen, Ein-Byte-Fälle, für die es geboren wurde; TextEncoder plus die chunk-weise Brücke für echten Text und Dateien in jedem Browser; Uint8Array.toBase64 mit seinen Alphabet- und Padding-Optionen für den modernen, direkten Weg; und die URL-sichere Variante, mit oder ohne Padding, für alles, was in einer URL leben wird. Der Rest ist Urteil: kennen Sie die 33-Prozent-Steuer, bevor Sie sie zahlen, halten Sie die Bytes in typed arrays, solange sie groß sind, erst kodieren und dann umbrechen, und nennen Sie ein Serialisierungsformat nie ein Schloss. Wenn der Kanal Roh-Bytes tragen kann, nehmen Sie die Bytes - Base64 ist für die Straßen, die nur druckbaren Text zulassen, und jetzt wissen Sie genau, wie man die Maut zahlt.
Die andere Hälfte der Reise - einen dieser Strings empfangen und Bytes, Text und Bedeutung wieder daraus herausholen - ist im Begleit-Leitfaden zum Base64-Dekodieren in JavaScript ausführlich behandelt, verlinkt unten.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Dekodierung in JavaScript/Browser: Ein vollständiger Leitfaden