Haben Sie mit dem Base64-Format zu tun? Dann ist diese Website genau das Richtige für Sie! Nutzen Sie unser superpraktisches Online-Tool, um Ihre Daten zu kodieren oder zu dekodieren.

Base64-Kodierung in JavaScript/Browser: Ein vollständiger Leitfaden

Sie haben etwas, das reisen muss, und die Straße ist nur breit genug für reinen ASCII. Es könnte ein Bild sein, das in eine JSON-Antwort gehört, ein Konfigurationsobjekt, das in einer URL mitfahren muss, ein Token, dessen drei Segmente aus Punkten und Buchstaben bestehen, eine Datei, die eine API als Base64-Zeichenkette in einem JSON-Body verlangt. Base64 ist die Mautstelle für genau diese Situation, und die Startseite dieser Site führt das Format bereits durch - vier druckbare Zeichen, die für je drei Bytes stehen, mit =-Padding, das die Gruppe abrundet - also hier die eine Zahl, die Sie beim Lesen im Kopf behalten sollen: Kodieren ist die wachsende Richtung. Jeder drei Bytes, die Sie hineingeben, kommen als vier Zeichen zurück, eine Größensteuer von etwa 33 Prozent, fällig in Bandbreite, Speicher und Arbeitsspeicher. Benutzen Sie Base64, wenn der Kanal druckbaren Text verlangt, und wissen Sie genau, was diese Steuer Sie kostet.

Die ermutigende Nachricht: Der Browser konnte diese Arbeit schon immer erledigen, ohne ein einziges Paket zu brauchen. btoa() gibt es seit Anfang der 2000er-Jahre, TextEncoder hat vor einem Jahrzehnt Ihren echten Unicode-Text in ehrliche Bytes verwandelt, und in der Baseline-2025-Welle bekam die Plattform endlich Uint8Array.toBase64() dazu, das Byte-Arrays direkt kodiert - mit Option für das URL-sichere Alphabet. Dieser Artikel ist die Entscheidungslandkarte: welches Werkzeug für welchen Job, wo die scharfen Kanten lauern (sie führen alle zurück zur selben 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, wahren Encoder, und gerade nach dem falschen zu greifen ist, wie die klassischen Bugs entstehen. Die Tabelle unten ist der gesamte Entscheidungsbaum:

Situation Greifen Sie zu
Reiner ASCII-Text, Einmal-Wert btoa(text)
Echter Text mit Akzenten, Emoji und CJK new TextEncoder().encode(text), dann btoa oder toBase64
Bytes, die schon in einer Uint8Array liegen bytes.toBase64() in Browsern ab 2025, woanders die btoa-Brücke in Chunks
URLs, JWTs, Dateinamen toBase64({ alphabet: 'base64url', omitPadding: true })
Ältere 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, zuerst ein Byte-Array werden, und genau dieses Byte-Array ist es, um das herum die modernen APIs gebaut wurden. Halten Sie sich "Text wird zu Bytes, Bytes werden zu Base64" im Kopf, und jedes Rezept in diesem Artikel ist dieselben zwei Schritte, nur mit anderen Namen.

btoa und die Latin1-Grenze

btoa(stringToEncode) - Binär-String zu ASCII-String - ist der Original-Encoder, verfügbar in jedem Browser, der zählt (Chrome 4, Firefox 1, Safari 3, IE 10 und neuer, alle Worker-Bereiche, und Node ab Version 16). Sein Vertrag hat eine Klausel, und genau diese Klausel ist der Punkt, an dem alles schiefgeht: 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) hindurch, während "你" (Codepunkt 20320) eine DOMException namens InvalidCharacterError wirft, bevor ein einziges Zeichen kodiert wurde. Die Grenze ist nicht "ASCII", sie ist nicht "Unicode", sie ist genau 256, und sie umfasst die Steuerzeichen am unteren Ende - 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
"" (leere Zeichenkette) "" - nichts rein, nichts raus
"\u0000" (NUL) "AA==" - Steuerzeichen sind Bürger erster Klasse
"a\u00e9z" (é, Codepunkt 233) "Yel6" - die gesamte Latin1-Reihe geht durch
"\u0100" (Codepunkt 256) wirft InvalidCharacterError - ein Schritt über die Grenze
"h\u4f60" (你, Codepunkt 20320) wirft InvalidCharacterError - und genauso jedes Emoji, denn sie alle liegen weit über 255

Zwei praktische Anmerkungen. Die Fehlermeldung unterscheidet sich je nach Engine - Firefox sagt "String contains an invalid character", Chrome sagt, die Zeichenkette "contains characters outside of the Latin1 range" - also fangen Sie in jeder defensiven Abwicklung nach dem Namen der Ausnahme ab. Und der Wurf passiert beim ersten üblen Zeichen, nicht am Ende: btoa kodiert nicht die halbe Zeichenkette und entschuldigt sich. Wenn Sie das Latin1-Verhalten gezielt wollen (einen Byte-String kodieren, der absichtlich aus Codepunkten 0-255 gebaut wurde), tut die Funktion genau das, was Sie gebeten haben, und die Tabelle oben ist ihre ganze 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 Dekoder 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-Konkatenation in einer Schleife ist langsam für große Dateien, und der beliebte Kurzweg - String.fromCharCode.apply(null, bytes), der das ganze Array in einem Aufruf als Argumente füttert - hat einen harten Abgrund. Funktionsaufrufe haben ein Limit für die Anzahl der Argumente, und es ist schon lange 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 einzelne Ä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 liefert eine Ansicht ohne Kopieren, und der Join produziert genau denselben Binär-String, den die Schleife produziert hätte. Jetzt die Textseite der Medaille. Für jeden echten Text verwandelt TextEncoder - der UTF-8-Encoder der Plattform, verfügbar in Firefox 18, Chrome 38, Safari 10.1 und seither überall - 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 der Leitung wirklich ist: sechs ASCII-Bytes plus sechs UTF-8-Bytes für die beiden chinesischen Zeichen, alle in derselben druckbaren Verkleidung. Wenn Ihr Text nicht UTF-8 ist - und im Web ist er es meistens - brauchen Sie zuerst den anderen Zeichensatz, was bedeutet, ihn irgendwo zu kodieren, das diesen Zeichensatz spricht, normalerweise der Server. TextEncoder weigert sich bewusst zu raten, und das mit Recht.

Der Kurzweg von 2025: Uint8Array.toBase64

Wenn Sie bereits eine Uint8Array in der Hand halten, 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 - in derselben Baseline-2025-Welle wie sein Dekodier-Geschwister - und es nimmt zwei Optionen entgegen, die es zum 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 abschließenden =-Zeichen werden weggelassen, was die Form ist, die die meisten URL-freundlichen Konsumenten wollen. Wird irgendetwas anderes als Optionen übergeben, wirft es eine TypeError; das ist die API, die höflich auf Ihren Tippfehler hinweist:

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="

Die beiden Bytes sind so gewählt, dass sie dem Alphabet maximal unfreundlich sind: Im Standard-Modus produzieren sie ein + und ein /, also zeigt die letzte Zeile genau, was sich ändert, wenn Sie zu base64url wechseln. Performance ist der stille Bonus: Auf einem aktuellen Firefox braucht das Kodieren von zehn Megabytes mit toBase64 etwa fünf Millisekunden, während der String-Brücken-Weg oben etwa fünfzehnmal so lange dauert, weil er dabei einen riesigen Zwischen-String baut. In älteren Browsern bleibt die Brücke für alles unter ein paar Megabytes völlig brauchbar - und die Chunk-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 nichts als Standard-Base64, das auf eine URL gestoßen ist. In einem Query-String ist + ein Leerzeichen; in einem Pfad ist / ein Trenner; und = verlangt in manchen Positionen Prozent-Kodierung. Das URL- und Dateinamen-sichere Alphabet aus RFC 4648, Abschnitt 5 - base64url - tauscht diese beiden Zeichen gegen - und _ aus, und da die Datengröße auf der Empfangsseite meist bekannt ist, erlaubt es auch, das Padding ganz wegzulassen. Die Ausgabe wandert durch URLSearchParams, Pfadsegmente, Fragmente und Dateinamen ohne ein einziges Prozentzeichen.

Mit der API von 2025 ist das ein Options-Objekt:

const params = new URLSearchParams();
params.set('payload', bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(params.toString()); // "payload=-_8" - gar keine Prozent-Kodierung

In älteren Browsern wandeln Sie nach dem Kodieren mit btoa um. Zwei Replace-Vorgänge 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 Familie, und kein Decoder wird raten, welche Sie gemeint haben. Padding ist ein Vertrag, kein Vorschlag: Lassen Sie es weg, muss der Empfänger für einen ungepaddeten Wert bereit sein, und behalten Sie es bei, muss der Empfänger nicht daran ersticken (Browser sind nachsichtig, manche JSON-Schemas sind es nicht). Und denken Sie daran, dass der Tausch umkehrbar und verlustfrei ist - - und _ landen an derselben 62. und 63. Alphabetposition, 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 werden 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 Benutzer gerade ausgesucht hat, erledigt FileReader die Kodierung 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 - macht canvas.toDataURL() diese Arbeit seit den frühesten 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 Stolperfallen, mit denen Sie rechnen sollten. Erstens: die tainted-Canvas-Regel: Wenn Sie ein Cross-Origin-Bild ohne CORS-Erlaubnis auf das Canvas gezeichnet haben, wirft jeder Versuch, Pixel zurückzulesen - einschließlich toDataURL - eine SecurityError. Der Fix besteht darin, das Bild mit crossOrigin = 'anonymous' zu laden und sicherzustellen, dass der Server die richtigen Header sendet. Zweitens: Das Qualitäts-Argument wird für PNG ignoriert und bedeutet nur bei JPEG (und WebP) etwas - 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 sitzt in der Seite als String. Für Bilder, die den Browser nie verlassen, gibt es eine kostenlose Alternative - eine Object-URL, die den Blob einpackt, ohne ihn überhaupt zu kodieren:

const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
URL.revokeObjectURL(objectUrl); // wenn Sie damit fertig sind

Die Arbeitsteilung, die sich daraus ergibt: Object-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 erzeugen - für einen selbst gehosteten Auth-Flow, eine Demo oder ein serverloses Frontend - besteht das kompakte JWS-Format aus drei base64url-Segmenten: Header, Payload, Signatur, nirgends Padding. Die Web Crypto API übernimmt das Signieren; die Kodierung 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 Rohrleitungen. Die Signatur deckt exakt header + '.' + payload ab - die rohen Segmente, nicht das JSON - also macht jede Änderung an einem der beiden Teile den Token ungültig, und genau darum geht es. Und crypto.subtle.sign gibt einen rohen ArrayBuffer zurück, daher die Einzeilen-Hülle in eine Uint8Array vor dem Segment-Encoder. Für RSA-basierte Tokens ist der Flow mit RS256 und einem Schlüssel-Paar identisch, und wenn Sie einen öffentlichen Schlüssel als JWK exportieren (crypto.subtle.exportKey('jwk', key)), kommen die numerischen Mitglieder - n, e und für Private Keys d, p, q - automatisch als ungepaddetes base64url heraus. Die Sicherheitshinweise sind dieselben wie bei jedem Token: Ein alg: "none"-Header ist eine Aufforderung, die Verifizierung zu überspringen, Zeit-Ansprüche (exp, nbf) müssen durchgesetzt werden, und ein Server, der für dieselbe Audience sowohl HMAC als auch RSA akzeptiert, öffnet die klassische Key-Confusion-Tür. Richtig kodieren, richtig signieren, auf der Empfangsseite verifizieren.

Authentifizierungs-Header

Das einfachste Authentifizierungsschema im Web ist zugleich das lehrreichste darüber, 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 Benutzername und Passwort 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 Header oben 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 die Formatierung. Für alles, was länger lebt als eine einzelne Anfrage, ziehen Sie tokenbasierte Schemata vor: Ein Bearer-Token ist ebenfalls nur ein Header, aber es ist ein zufälliger Wert, dessen Geheimnis gar nicht erst im Header mitgeführt werden muss, und es kann widerrufen werden. Die Kodierungs-Entscheidung zwischen den beiden ist trivial - beides ist btoa oder Klartext - aber die Sicherheitsentscheidung ist das 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 beziffert wird, denn die Datei ist normalerweise das Größte auf der Seite. Es gibt zwei Straßen, und die erste ist die, die Sie standardmäßig nehmen sollten: multipart-Formdaten. FormData transportiert die Datei als rohe Bytes in einem Standard-Body, der Browser erledigt das Framing, und Base64 ist nirgends im Spiel - keine Größensteuer, kein Zwischen-String, und die Bytes streamen zum Server, so wie sie gelesen werden:

const form = new FormData();
form.append('upload', file);
await fetch('/api/upload', { method: 'POST', body: form });

Die zweite Straße 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 die Kodierung 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. Für ein Foto in Ordnung, für ein Video schmerzhaft:

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 dieser Straße bauen Sie keinen einzigen riesigen String in einem Aufruf - bauen Sie ihn in Scheiben, wobei jede Scheibe ein Vielfaches von drei Bytes ist. Diese Ausrichtung ist es, was den Trick legal macht: Ein Vielfaches von drei Bytes kodiert zu einem sauberen Vielfachen von vier Zeichen ohne Padding, also fügen sich unabhängig kodierte Scheiben zu exakt der Kodierung der ganzen Datei zusammen, und nur die letzte Scheibe trägt überhaupt 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('');
}

Dieselbe Ausrichtungsidee ist der Grund, warum Sie eine Base64-Zeichenkette nie an einem beliebigen Punkt teilen und erwarten sollten, die Stücke würden für sich allein dekodiert - 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 Blob plus Object-URL der gesunde Weg, denn der Browser muss den ganzen Payload gar nicht erst als String mitführen.

Zustand speichern und teilen

Noch zwei weitere reine Textkanä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 reinkommen. Der Round-Trip ist ein Kodieren und ein Dekodieren, und es lohnt sich, beide Seiten zusammen zu sehen, denn ein Storage-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 es aber richtig: Die Origin bekommt etwa 5 Megabyte an localStorage, Ihr gespeicherter String ist 33 Prozent dicker als die Daten, und solange die Seite offen ist, lebt der String zusätzlich im Speicher als UTF-16 - wieder doppelt so lang. Ein 3-Megabyte-Asset ist 4 Megabyte Speicher und 8 Megabyte Arbeitsspeicher, und so wird aus einer "kleinen" Funktion ein Quota-Fehler. Der zweite Kanal ist die URL selbst: Teilen-Links, Deep-Links und OAuth-Zustand wollen alle strukturierte Daten an einem Ort, der Copy-Paste übersteht. Das Rezept ist kompakter Zustand, JSON, dann base64url ohne Padding, damit der Wert gar keine Prozent-Kodierung braucht - und halten Sie die ganze URL unter ein paar tausend Zeichen, denn dort fangen ältere Clients, Proxys und Logging-Tools an, nervös zu werden.

E-Mail und MIME

Base64 ist älter als das Web, und seine Heimat ist die E-Mail. MIME-Anhänge mit Content-Transfer-Encoding: base64 sind die Art, wie eine binäre Datei in einem Textprotokoll mitfährt, und die Konvention, die sich aus der alten 76-Zeichen-Limitierung des Nachrichtenformats ergab, lohnt sich zu kennen: Den kodierten Body alle 76 Zeichen pro Zeile umbrechen. Der Browser kann kein SMTP senden, aber er erledigt zwei E-Mail-Jobs - MIME-Bodies bauen, die ein Backend-Relay senden wird, und die Anhänge empfangener Nachrichten anzeigen - und beide berühren die Kodierung. Der Umbruch selbst ist eine Zweizeilen-Funktion, und die Reihenfolge der Operationen zählt: Erst kodieren, dann umbrechen, denn btoa wirft bei einem Zeilenumbruch in seiner Eingabe keine Ausnahme - 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 Empfangsseite ist die umsonst: atob überspringt ASCII-Leerraum als Teil seines Standardverhaltens, also dekodiert ein umgebrochener MIME-Body genau so, wie er ankam, Zeilenumbrüche inklusive, ganz ohne Unwrap-Schritt. Wenn Sie einen Webmail-Client oder einen Anhang-Auswahl-Bildschirm bauen, ist diese eine Asymmetrie - der Encoder muss saubere Zeilen produzieren, dem Decoder ist es egal - die ganze MIME-Geschichte in einem Satz.

Wann man zu einer Bibliothek greifen sollte

Mit den nativen Werkzeugen oben ist eine Bibliothek selten nötig, und die ehrliche Empfehlung lautet: Standardmäßig die Plattform nutzen und ein Paket nur hinzufügen, wenn eine echte Anforderung auf eines verweist. Die drei, die in Codebasen tatsächlich auftauchen:

js-base64 (npm install js-base64) ist der Universal-Player: ein kleiner, reiner JavaScript-Transcoder, der UTF-8-Strings als Bürger erster Klasse behandelt - Base64.encode auf einem CJK-String macht den UTF-8-Tanz für Sie - und, nützlich fürs Dekodieren ebenso wie fürs Kodieren, beide Alphabete in decode akzeptiert und eine isValid-Prüfung mitliefert. Es ist die richtige Antwort, wenn Sie Browser targeten, in denen die APIs von 2025 fehlen, und Sie mit einem Import Strings und Bytes abdecken wollen:

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 nach wie vor eine gute Wahl, wenn Ihr Code in Typed Arrays lebt und Sie möchten, dass die Kodierung eine reine Funktion der Bytes ist. Und wenn Ihr Grund, eine Bibliothek zu wollen, lautet "Ich mag die API von 2025, kann aber keine Browser von 2025 verlangen", dann ist die Antwort gar kein Base64-Paket, sondern ein Polyfill: core-js (und das Babel-Preset, das es hereinholt) implementiert Uint8Array.fromBase64 und Freunde, so können Sie den Code im neuen Stil einmal schreiben und den Shim die Lücke auf älteren Engines füllen lassen. Wählen Sie nach Einschränkung - ältere Browser, String-Komfort oder Byte-Pureness - nicht nach Gewohnheit.

Stolperfallen, die Entwicklern Stunden kosten

  • btoa auf einem String mit einem Zeichen über Codepunkt 255 aufrufen. Es wirft eine Ausnahme, es entstellt nicht, und es bleibt beim ersten Übeltäter stehen. Der Fix ist immer derselbe: Erst TextEncoder, dann die Brücke.
  • Der fromCharCode.apply-Abgrund bei großen Arrays. Eine Million Argumente ist ein RangeError in beiden großen Engines. Die Brücke in Chunks teilen, oder zu toBase64 wechseln.
  • Die Größensteuer vergessen, wo sie am meisten wehtut: im Speicher. Eine Datei in localStorage ist 33 Prozent größer als die Datei, und die Quote ist pro Origin und wird 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 die ganze Bug-Gattung verschwindet.
  • Inkonsistentes Padding über Dienste hinweg. Ein Gateway behält das =, ein anderes streicht es, ein drittes setzt es wieder hinzu. Der Empfänger muss für beide Formen bereit sein, und der Vertrag sollte sagen, welche die maßgebliche ist.
  • Base64 als Schloss behandeln. Es ist ein Serialisierungsformat, ein Funktionsaufruf entfernt von Klartext, und "mit Base64 kodiert" in einer Sicherheitsprüfung ist ein Befund, keine Kontrolle.
  • Binär-Strings als Speicher-Modell. Ein dekodiertes oder kodiertes Megabyte reist in UTF-16 mit zwei Megabyte; eine Uint8Array hält es mit einem. Bei großen Payloads die Bytes von Anfang bis Ende in Typed Arrays lassen.
  • Doppel-Kodierung. Ein Wert, der schon Base64 war, wird noch einmal kodiert, und der Konsument dekodiert einmal und bekommt einen String aus Buchstaben statt Daten. Wenn Sie unsicher sind, prüfen Sie vor dem Einpacken - ein String, der schon im Alphabet mit gültigem Padding ist, ist ein Code-Smell.
  • Dem JWT-Payload vertrauen, weil er sich sauber dekodiert. Dekodierbarkeit ist keine Echtheit. Verifizieren Sie die Signatur mit dem richtigen Schlüssel und dem richtigen Algorithmus, bevor Sie einen einzigen Anspruch lesen.

Performance: Was eine Million Bytes kostet

Base64 im Browser ist dort billig, wo es teuer war, und das Budget hat jetzt drei Posten statt eines. CPU: Auf einem aktuellen Firefox kodiert Uint8Array.toBase64 zehn Megabytes in etwa fünf Millisekunden, während die btoa-Brücke in Chunks etwa fünfzehnmal so lange braucht - nicht weil btoa langsam ist, sondern weil die Brücke dabei einen riesigen Zwischen-String baut. Wenn Ihr Kodier-Budget in Millisekunden liegt, nutzen Sie die native Methode; wenn Sie ein 2-Kilobyte-Konfigurationsobjekt kodieren, sind beide unterhalb der Wahrnehmungsschwelle. Bandbreite: Das ist die dauerhafte Steuer - jedes Byte, das Sie kodieren, kostet 1,33 Bytes auf der Leitung, plus alles Framing, das der Transport hinzufügt. Messen Sie den Transfer, bevor Sie die Kodierung "optimieren". Speicher: Der kodierte String ist die größte temporäre Allokation, die Sie tätigen, und für eine 5-Megabyte-Datei ist es ein 6,7-Megabyte-String, oder etwa 13,4 Megabyte an UTF-16-Speicher, solange die Seite ihn hält. Die praktischen Konsequenzen ergeben sich aus der Arithmetik: Große Kodierungen in Scheiben teilen, damit kein einzelner String riesig wird, die Zwischen-Bytes frei geben, sobald der String existiert, Object-URLs und multipart bevorzugen, wenn die Bytes nie druckbar sein mussten, und Mehr-Megabyte-Arbeit zu einem Web Worker verschieben, wenn der Haupt-Thread weiter glatt scrollen muss. Das Format ist fast vier Jahrzehnte alt; die Plattform ist ihm endlich hinterhergekommen.

Wie Browser das Kodieren gelernt haben

Der Encoder hat eine Geschichte, und sie erklärt die Relikte, die Sie erben werden. btoa - "von Binär zu ASCII", der Name ist wörtlich gemeint, und atob ist einfach dieselben Wörter umgekehrt - wurde 2011 in die HTML-Spezifikation geschrieben, rückwärtsingenieuert aus den Browsern, die sie bereits ausgeliefert hatten: Firefox ab 2004, Safari 3, Chrome 4. Internet Explorer, charakteristisch wie immer, übersprang beide Funktionen bis Version 10 im Jahr 2012, und genau diese eine Lücke ist der Grund, warum ein Jahrzehnt JavaScript voller handgebauter Base64-Tabellen und einer bestimmten Beschwörung für Unicode ist: btoa(unescape(encodeURIComponent(str))). Es funktionierte - encodeURIComponent erzeugt Prozent-escaped UTF-8, und unescape machte daraus einen Byte-String - aber es wurde auf unescape() aufgebaut, dem Mitglied des Paares, das die Sprache deprecated hatte, und es überlebte jahrelang im Browser-Code aus reiner 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 gar keiner Version von IE - 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 von Tag eins, 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 aus - Uint8Array.toBase64 und 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. Die lustigen Trivia am Ende des Artikels handeln vor allem davon, wie lange jedes Teil auf sich warten ließ.

Wussten Sie schon?

  • Die Funktionsnamen sind ein Satz: btoa ist "von Binär zu ASCII" und atob ist "von ASCII zu Binär". Die Richtung steht im Namen, darum ist das Paar seit den 2000er-Jahren selbst dokumentierend.
  • Der am häufigsten kodierte String in der Geschichte der Informatik ist wahrscheinlich "hello": btoa('hello') ergibt aGVsbG8= - die Ausgabe von jedem Tutorial, jeder Test-Suite und jedem Interview-Whiteboard auf dem Planeten.
  • Jede gültige Base64-Zeichenkette hat eine Länge, die ein Vielfaches von vier ist, Padding inklusive. Die =-Zeichen sind ein Fingerabdruck: Eines bedeutet, die letzte Gruppe hielt zwei Bytes, zwei bedeuten, sie hielt eines.
  • Der 76-Zeichen-Zeilenumbruch in MIME und in den meisten Kommandozeilen-Werkzeugen ist eine Erbschaft aus der E-Mail-Ära, als die Zeilenlänge des Nachrichtenformats das Limit setzte. Die Zahl hat drei Jahrzehnte von schnellerem allem überlebt.
  • "Data URI" ist ein ausrangierter Name. Das WHATWG hat es während der großen URI-zu-URL-Harmonisierung zu "data URL" umbenannt, weshalb Spezifikationen, Blog-Posts und Paketnamen im selben Absatz alle unterschiedlich schreiben.
  • btoa('') gibt '' zurück: eine leere Eingabe produziert eine leere Ausgabe, kein Padding, kein Sonderfall - die einzige Base64-Zeichenkette mit null Zeichen (ihre Länge, 0, ist immer noch ein Vielfaches von vier).
  • Ein Canvas kann ein Foto mit toDataURL in eine Data-URL verwandeln - eine Fähigkeit, die seit IE 9, Firefox 2 und Safari 4 existiert und einen Großteil des Web-Platforms vorwegnimmt, das wir für "modern" halten - und das Ganze per Round-Trip mit einem <img>-Tag und einem FileReader zurü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, dass kein reiner HTTP-Server den Handshake je versehentlich abschließen konnte.

Der nächste Schritt

So passt die ganze Kunst des Kodierens im Browser auf eine Seite: btoa für die einfachen, Ein-Byte-Fälle, für die es geboren wurde; TextEncoder plus die Brücke in Chunks 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: Die 33-Prozent-Steuer kennen, bevor man sie zahlt, die Bytes solange in Typed Arrays lassen, wie sie groß sind, erst kodieren und dann umbrechen, und ein Serialisierungsformat nie ein Schloss nennen. Wenn der Kanal rohe Bytes tragen kann, nehmen Sie die Bytes - Base64 ist für die Straßen, die nur druckbaren Text hereinlassen, und nun wissen Sie genau, wie die Maut zu zahlen ist.

Die andere Hälfte der Reise - eine dieser Zeichenketten empfangen und die Bytes, den Text und die Bedeutung wieder herausziehen - ist im Begleitguide zur Base64-Dekodierung in JavaScript im Detail abgedeckt, verlinkt unten.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Dekodierung in JavaScript/Browser: Ein vollständiger Leitfaden