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-Dekodierung in JavaScript/Browser: Ein vollständiger Leitfaden

Es kommt in einem Dutzend verschiedenen Verkleidungen: ein JWT, versteckt in einem Authorization-Header, ein image/png-Blob in einer JSON-Antwort, ein Sec-WebSocket-Accept-Wert in einem Handshake-Log, ein E-Mail-Anhang, eingewickelt in MIME, ein Wert, den Ihr Backend höflich in einen Query-String gesteckt hat. Der String selbst sieht immer gleich aus: eine lange Kette aus Buchstaben und Ziffern, hin und wieder ein + oder /, und vielleicht ein = oder zwei am Ende. Wenn Ihnen die Startseite dieser Site erklärt hat, was Base64 ist - vier druckbare Zeichen als Stellvertreter für jeweils drei Bytes, mit =-Padding, um die letzte Gruppe abzuschließen - dann geht es in diesem Artikel um den Teil, den Sie tatsächlich im Code erledigen: aus diesen Zeichen wieder Bytes zu machen und aus den Bytes wieder Bedeutung, und zwar nur mit dem, was der Browser schon mitbringt.

Zwei kurze Grundregeln, bevor wir loslegen. Erstens: Dekodieren ist die schrumpfende Richtung. Für vier Zeichen, die Sie hineinlesen, kommen drei Bytes heraus, das Ergebnis belegt also immer weniger Speicher als die Eingabe. Zweitens: Ein dekodierter Base64-String ist nicht automatisch Text. Es sind Bytes, und Bytes können sich als UTF-8, Windows-1252, ein PNG-Header oder eine kryptografische Signatur herausstellen. Der häufigste Bug in Base64-Code entsteht, wenn man vergisst, welches von all dem man gerade in der Hand hält, daher sind die folgenden Abschnitte um genau diese Frage aufgebaut.

Die drei Ebenen beim Dekodieren

Der moderne Browser bietet drei native Ebenen, und die gute Nachricht: Es wird nie ein Paket benötigt. Jede beantwortet eine etwas andere Frage, und die richtige Wahl erspart Ihnen viele kopierte Stack-Overflow-Snippets:

Ebene Was es frisst Was es liefert Persönlichkeit Verfügbarkeit
atob() Standard-Base64-String ein "Binärstring" (ein Byte pro Zeichen) sehr nachsichtig: überspringt ASCII-Leerzeichen, akzeptiert fehlendes Padding jeder Browser seit den 2000ern, IE 10+, Node 16+
TextDecoder Bytes (Uint8Array) lesbarer JavaScript-Text konfigurierbar: Label für den Zeichensatz, fatal-Flag für Strenge Firefox 18, Chrome 38, Safari 10.1 und neuer (nie IE)
Uint8Array.fromBase64() Base64-String plus Optionen eine echte Uint8Array streng mit Stellschrauben: Alphabet und Behandlung des letzten Chunks Baseline 2025: Chrome 140, Firefox 133, Safari 18.2, Node 25

Der Aufbau des ganzen Artikels ergibt sich aus dieser Tabelle. atob() ist das Arbeitstier, das Sie überall antreffen, auch in altem Code. TextDecoder ist die Brücke von Bytes zu Wörtern. Und Uint8Array.fromBase64() ist das 2025er-Upgrade, das den Zwischenschritt komplett überspringt, wenn Sie ohnehin nur Bytes wollten.

atob: Schnell, nachsichtig und sehr alt

Der gesamte Vertrag passt in eine Zeile: atob(encodedData). Die Funktion nimmt einen Base64-kodierten String entgegen und gibt einen "Binärstring" zurück: einen ganz normalen JavaScript-String, in dem jedes Zeichen exakt ein dekodiertes Byte hält, einen Codepunkt von 0 bis 255. Dieser Rückgabetyp ist wichtig, denn er ist nicht dasselbe wie lesbarer Text (mehr dazu weiter unten). Die Funktion selbst ist so schnell wie nur möglich, und sie ist schon seit sehr langer Zeit dabei: Chrome 4, Firefox 1, Safari 3, und - das ist der Punkt, den die meisten im Kopf behalten - Internet Explorer erst ab Version 10. Deshalb ist Code, der vor 2012 geschrieben wurde, voller selbst gebastelter Base64-Tabellen.

Was atob() angenehm macht, ist, wie viel es verzeiht, bevor es aufgibt. Der WHATWG-HTML-Standard sagt, dass vor dem Dekodieren alle ASCII-Leerzeichen ignoriert werden sollen - Leerzeichen, Tabulator, Zeilenumbruch, Seitenvorschub, Carriage Return - daher dekodiert ein MIME-umgebrochener String mit einem Zeilenumbruch alle 76 Zeichen, ohne dass Sie überhaupt etwas aufbereiten müssen. Auch fehlendes Padding wird verziehen. Aber in dem Moment, in dem es ein Zeichen außerhalb des Alphabets sieht, oder eine Länge, die niemals gültig sein könnte, wirft es eine DOMException namens InvalidCharacterError. Kein stiller Müll, keine Teilergebnisse.

Hier ist der Schadensreport, Zeile für Zeile:

Eingabe Ergebnis
"SGVsbG8sIFdvcmxkIQ==" "Hello, World!" - der Lehrbuchfall
"aGVsbG8" (ohne Padding) "hello" - ein fehlendes = wird verziehen
"SGVs\nbG8s\nIFdvcmxkIQ==" (umgebrochene Zeilen) "Hello, World!" - ASCII-Leerzeichen werden zuerst übersprungen
"" (leerer String) "" - die leere Eingabe ist gültig und kommt sauber zurück
"A" (ein übrig gebliebenes Zeichen) wirft InvalidCharacterError - ein einziges Zeichen kann nichts kodieren
"Zm9vYmFy!" (ein verirrtes !) wirft InvalidCharacterError - außerhalb des Alphabets
"ZGFua29nYWk-" (URL-sicheres Zeichen vermischt) wirft InvalidCharacterError - die beiden Alphabete dürfen nicht gemischt werden
"Zm9v====" (zu viel Padding) wirft InvalidCharacterError - höchstens zwei = am Ende

Ein praktischer Hinweis: Die Fehlermeldung selbst unterscheidet sich zwischen den Engines (Firefox sagt "String contains an invalid character", Chrome sagt für nicht-Latin1-Eingaben, der String "contains characters outside of the Latin1 range", und bei ungültigem Base64 "is not correctly encoded"). Fangen Sie also anhand des Ausnahme-Namens ab, nicht anhand des Meldungstextes.

Von Roh-Bytes zu echtem Text

Der Rückgabetyp "Binärstring" verdient einen Satzpunkt, denn er ist die Ursache für die meisten Dekodier-Verwirrungen. JavaScript-Strings sind UTF-16, daher reicht atob() Ihnen einen String, dessen Zeichen Byte-Werte sind und keine lesbaren Glyphen. Wenn Ihr Payload die UTF-8-Kodierung des Textes "hello 你好" war, liefert das direkte Ausgeben des Ergebnisses Mojibake. Die Lösung ist ein zweistufiges Dekodieren: zuerst Base64 zu Bytes, dann Bytes zu Text.

Erst einmal der Schritt von Base64 zu Bytes. Diese kleine Hilfsfunktion ist das klassische Rezept und gehört in jede Hosentasche, denn sie ist das tragende Stück in den meisten Beispielen dieses Artikels:

function base64ToBytes (base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i += 1) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}

Dann der Schritt von Bytes zu Text, mit TextDecoder. Für UTF-8 (der Standard und die richtige Wahl für JSON, JWT-Payloads und die meisten Webdaten) ist der Aufruf eine Zeile:

const bytes = base64ToBytes('aGVsbG8g5L2g5aW9');
const text = new TextDecoder('utf-8').decode(bytes);
console.log(text); // "hello 你好"

Warum überhaupt zwei Schritte? Weil atob() keine Ahnung hat, in welchem Zeichensatz die Bytes entstanden sind. Es ist ein reiner Bit-Wandler. TextDecoder ist die Komponente, die Bytes als Zeichensatz interpretiert, und es akzeptiert dafür ein Label: utf-8, windows-1252, iso-8859-1, utf-16le plus rund 220 weitere Labels. Daten, die aus einer Anwendung der 1990er Jahre stammen, sind in der Regel Windows-1252, und ein einzelner Konstruktor-Parameter reicht dafür:

const decoder = new TextDecoder('windows-1252');
const text = decoder.decode(bytes); // dieselben Bytes, andere Interpretation

Der Konstruktor von TextDecoder akzeptiert außerdem ein fatal-Flag, und es lohnt sich, es auf true zu setzen, wann immer der dekodierte Text etwas Wichtiges speist. Standardmäßig ist der Dekodierer nachsichtig: Ungültige Byte-Folgen werden still und leise durch das Unicode-Ersatzzeichen U+FFFD ersetzt, und Sie erfahren nichts davon. Mit fatal: true wirft derselbe Schaden stattdessen eine TypeError, anstatt ihn zu verstecken:

const strict = new TextDecoder('utf-8', { fatal: true });
try {
  strict.decode(corruptedBytes);
} catch (error) {
  console.log(error.name); // "TypeError"
}

Das ist einer dieser Schalter, die in der Doku nebensächlich wirken, in Produktion aber wie ein Daten-Vorfall aussehen. Wenn Ihre Eingabe vom Nutzer oder vom Netz kommt, dekodieren Sie strikt und behandeln Sie den Fehler bewusst.

URL-sichere Eingaben brauchen einen Umweg

Eine Variante von Base64 verdient einen eigenen Abschnitt, denn sie taucht ständig in der Wildnis auf, und atob() liest sie nicht. Es ist das URL- und Dateinamen-sichere Alphabet aus Abschnitt 5 von RFC 4648, üblicherweise base64url genannt: dieselben 64 Zeichen, nur dass + und / durch - und _ ersetzt werden, und das =-Padding wird oft weggelassen, weil die Datengröße implizit bekannt ist. Der Tausch hat einen konkreten Grund: In einer URL bedeutet + ein Leerzeichen, und / beginnt ein Pfadsegment. Das Standard-Alphabet müsste also Zeichen für Zeichen prozent-kodiert werden. Base64url reist sauber durch Query-Strings, Pfadsegmente, Fragmente und Dateinamen.

Der Haken: Die beiden Alphabete sind nicht austauschbar, und atob() spricht nur das Standard-Alphabet. Geben Sie ihm ein - oder _, und Sie bekommen InvalidCharacterError. Sie haben zwei saubere Optionen.

Option eins, die überall funktioniert: das Alphabet umwandeln und das Padding wiederherstellen, bevor Sie atob() aufrufen:

function fromUrlBase64 (segment) {
  let s = segment.replace(/-/g, '+').replace(/_/g, '/');
  const missing = (4 - (s.length % 4)) % 4;
  return atob(s + '='.repeat(missing));
}
console.log(fromUrlBase64('aGVsbG8')); // "hello"

Der Ausdruck (4 - (s.length % 4)) % 4 ist der ganze Trick: Er berechnet, wie viele =-Zeichen ein korrekt gepadderter String dieser Länge bräuchte, von null bis zwei.

Option zwei, in Browsern von 2025 und neuer: Der neue native Dekodierer nimmt das Alphabet als Option entgegen, ganz ohne Eingriffe am String:

const bytes = Uint8Array.fromBase64('P3-0', { alphabet: 'base64url' });
console.log(Array.from(bytes).join(', ')); // "63, 127, 180"

Zwei Regeln halten Sie aus Schwierigkeiten heraus. Mischen Sie nie Alphabete innerhalb eines einzigen Werts - ein Dekodierer, der sowohl + als auch - sieht, hat keine Möglichkeit zu wissen, welche Familie er liest, und das spezifikationskonforme Verhalten ist, zu scheitern. Und vereinbaren Sie mit der anderen Seite der Leitung, ob Padding vorhanden ist oder nicht: Es wegzulassen ist für base64url erlaubt, also muss ein Empfänger für beide Formen bereit sein. atob() ist es bereits; die nativen Optionen unten geben Ihnen dafür eine Stellschraube.

Der 2025er-Shortcut: Uint8Array.fromBase64

Werfen Sie einen Blick zurück auf den base64ToBytes-Helfer, und Sie werden merken, dass er zwei Dinge tut: Er dekodiert Base64 und kopiert dann die Zeichen in JavaScript einzeln in ein Byte-Array. Diese Kopierschleife ist der langsame, vermeidbare Teil, und genau den eliminiert die neue ECMAScript-Methode. Uint8Array.fromBase64(string, options) geht direkt vom kodierten String zu einem Byte-Array, und sie ist in Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 und Deno 2.5 enthalten - das erste Feature dieser Art auf der JavaScript-Plattform, das gelandet ist, im Baseline-Programm der Browser-Hersteller als Baseline Newly available markiert.

Das Options-Objekt hat zwei Stellschrauben. Die erste ist alphabet: "base64" (der Standard) oder "base64url". Die zweite ist lastChunkHandling, sie steuert, was mit der letzten unvollständigen Zeichen-Gruppe geschieht:

Modus Regel für den letzten Chunk
"loose" (Standard) zwei oder drei Zeichen, oder vier mit Padding; übrig bleibende Overflow-Bits werden ignoriert
"strict" exakt vier Zeichen (Padding nur, wo die Länge es verlangt), und die Overflow-Bits müssen alle null sein
"stop-before-partial" nur vollständige Vierer-Gruppen werden dekodiert; ein unvollständiger Rest bleibt ungelesen

Wie atob() ignoriert die Methode ASCII-Leerzeichen in der Eingabe, also sind umgebrochene Zeilen unproblematisch. Im Gegensatz zu atob() hat sie aber eine Meinung zu allem anderen: Ein Zeichen außerhalb des gewählten Alphabets oder ein letzter Chunk, der den gewählten Modus verletzt, wirft eine SyntaxError. Etwas, das kein String ist, wirft eine TypeError. Hier ist der strict-Modus in Aktion und lehnt einen Chunk ab, dem das Padding fehlt:

const ok = Uint8Array.fromBase64('SGVsbG8=', { lastChunkHandling: 'strict' });
try {
  Uint8Array.fromBase64('VR', { lastChunkHandling: 'strict' });
} catch (error) {
  console.log(error.name); // "SyntaxError"
}

Performance ist der andere Grund, sie zu bevorzugen. Auf einem aktuellen Firefox auf der Maschine des Autors dekodiert fromBase64 einen 10-Megabyte-Payload in einstelligen Millisekunden, während das klassische atob plus die zeichenweise Byte-Zuordnung etwa zwanzigmal so lange braucht. Denn der langsame Teil ist die Schleife auf JavaScript-Ebene, nicht die Base64-Mathematik. Wenn Ihre Daten Bytes sind, überspringen Sie den String komplett.

Für ältere Browser ist die Lage einfach: Behalten Sie den base64ToBytes-Helfer oben bei, oder ziehen Sie ein kleines Polyfill heran (core-js und das es-arraybuffer-base64-Paket aus dem es-shims-Projekt liefern jeweils eines für fromBase64), wenn Sie überall im neuen Stil schreiben wollen. Die API ist stabil - sie steht jetzt in der ECMAScript-Spezifikation - also wird nichts, was Sie dagegen schreiben, obsolet werden.

Ein JWT lesen

Der häufigste "mysteriöse String" in Anwendungs-Logs ist ein JSON Web Token: drei durch Punkte getrennte Segmente, header.payload.signature, wobei die ersten zwei base64url-kodierte JSON-Objekte sind. Eines davon zu dekodieren ist eine Fünf-Zeilen-Sache und ein perfektes Warm-up für alles bisher Gesehene:

function jwtSegmentToBytes (segment) {
  let s = segment.replace(/-/g, '+').replace(/_/g, '/');
  s += '='.repeat((4 - (s.length % 4)) % 4);
  return base64ToBytes(s);
}
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const [header64, payload64] = token.split('.');
const payload = JSON.parse(new TextDecoder().decode(jwtSegmentToBytes(payload64)));
console.log(payload.name); // "John Doe"

Jetzt der Teil, den Anfänger überspringen und Produktionssysteme auf die harte Tour lernen: Der Payload wird nicht dadurch verifiziert, dass er dekodierbar ist. Jeder kann ein JWT mit beliebigem Payload schreiben; das Signatur-Segment ist es, das ihn mit einem Geheimnis verbindet. Die Verifikation eines HS256-Tokens im Browser verwendet die Web Crypto API, die die Signatur als Bytes braucht - noch ein Grund, warum sich der Segment-zu-Bytes-Helfer lohnt:

const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
  'raw',
  encoder.encode('shared-secret'),
  { name: 'HMAC', hash: 'SHA-256' },
  false,
  ['verify']
);
const [h, p, sig64] = token.split('.');
const valid = await crypto.subtle.verify(
  'HMAC',
  key,
  jwtSegmentToBytes(sig64),
  encoder.encode(h + '.' + p)
);
console.log(valid); // true nur, wenn die Signatur zum Geheimnis passt

Drei Fallen verdienen einen Namen. Erstens: Prüfen Sie den Header, bevor Sie verifizieren. Ein Token, das alg: "none" behauptet, bittet Sie, den Payload ohne Signatur zu vertrauen, und naiver Code hat sich genau darauf eingelassen. Zweitens: Beachten Sie die Zeit-Claims - exp, nbf, iat - nach der Verifikation, nicht vorher. Drittens: der klassische Key-Confusion-Angriff. Ein Server, der für RS256 konfiguriert ist, aber auch HS256 akzeptiert, lässt einen Angreifer Tokens mit dem öffentlichen Schlüssel signieren (der ist aus gutem Grund öffentlich), verwendet als HMAC-Geheimnis. Kurz gesagt: Dekodieren Sie frei, vertrauen Sie nichts, verifizieren Sie alles.

Data-URLs öffnen

Eine Data-URL steckt eine komplette Datei in eine URL: data:, ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma, dann der Payload. Text-Payloads sind prozent-kodiert, binäre Payloads sind Base64, und der Browser rendert sie ohne jegliche HTTP-Anfrage - kein fetch, kein Server-Rundweg, nichts zum Cachen. Der Browser behandelt jede Data-URL als einzigartigen, undurchsichtigen Origin, was auch der Grund ist, warum sie ein beliebter Vektor für dubioses Content sind: Eine im iframe geöffnete data:text/html-Seite führt ihre Skripte aus, und eine restriktive Content-Security-Policy kann Data-URLs ganz blockieren. Halten Sie Ihre CSP im Blick, wenn Sie damit anfangen, sie in nutzerkontrolliertes Markup zu geben.

So eine zu dekodieren ist größtenteils String-Handarbeit, dann dieselbe Byte-Pipeline wie zuvor:

const url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADgQFY/fWoOgAAAABJRU5ErkJggg==';
const comma = url.indexOf(',');
const meta = url.slice(5, comma); // "image/png;base64"
const bytes = base64ToBytes(url.slice(comma + 1));
const blob = new Blob([bytes], { type: 'image/png' });
const objectUrl = URL.createObjectURL(blob);

Das meta-Stück sagt Ihnen den Medientyp (hier image/png, mit der ;base64-Marke, die bestätigt, dass der Payload Base64 ist). Sobald der Payload ein Blob ist, gelten die normalen Regeln: eine Objekt-URL für ein <img>, ein Download-Link oder ein POST an einen Server. Die einzige echte Kosten der Data-URL-Spur ist die Größe - der Payload ist etwa 33 Prozent größer als die Originaldatei - und ein großes Bild in einer URL kann die String-Limits der Seite an ihre Grenzen bringen. Ein weiteres Argument für Objekt-URLs, wenn die Datei nie den Browser verlassen muss.

Dateien dekodieren, die als Text ankommen

Dateien erreichen den Browser auf zwei Wegen. Der moderne Weg sind Roh-Bytes: ein fetch, das Sie als ArrayBuffer lesen, oder eine File aus einem Datei-Picker, die Sie mit file.arrayBuffer() lesen. Wenn Sie auf diesem Weg sind, herzlichen Glückwunsch - Base64 kommt hier überhaupt nicht ins Spiel, und Sie sollten auch auf diesem Weg bleiben. Bytes kosten beim Transport nichts, während Base64 für dieses Privileg ein Drittel extra an Bandbreite und Speicher kostet. Der andere Weg ist, wenn der Kanal nur Text erlaubt: eine JSON-API, die {"attachment": "data:application/pdf;base64,JVBERi..."} zurückgibt, ein E-Mail-Anhang, ein Konfigurations-String, ein Wert in einer Datenbank-Spalte. Dann ist Base64 das Protokoll, und Ihre Aufgabe besteht nur darin, die Bytes herauszuholen:

async function loadRemoteBytes (fileUrl) {
  const response = await fetch(fileUrl);
  return new Uint8Array(await response.arrayBuffer());
}
const record = JSON.parse(await (await fetch('/api/record/42')).text());
const pdfBytes = base64ToBytes(record.attachment.split(',')[1]);

Drei Anmerkungen zu diesem Snippet. Am ersten Komma zu trennen genügt, um den Data-URL-Header abzuschälen (der Medientyp kann kein Komma enthalten, also ist das erste immer der Trenner). Und wenn der Wert einfach Base64 ohne Data-URL-Präfix ist, lassen Sie die Trennung einfach weg. Schließlich: Dieses nackte await ist ein top-level Await, und Browser erlauben das nur innerhalb von Modulen. Das Snippet braucht also ein <script type="module">-Tag oder einen async-Wrapper um diese beiden Zeilen. E-Mail-MIME-Teile sind dieselbe Geschichte mit zusätzlichen Schritten: Der Anhang-Inhalt ist Base64, umgebrochen auf 76 Zeichen pro Zeile. Da atob() aber Leerzeichen überspringt, können Sie ihm den umgebrochenen Text genau so reichen, wie er in der rohen Nachricht ankam - kein Entpacken nötig. Dieses eine Verhalten spart still und leise eine Menge Regex.

Ein WebSocket-Handshake verifizieren

Eine der charmantesten Anwendungen des Dekodierens im Browser ist die Prüfung des WebSocket-Handshakes selbst. RFC 6455 verlangt, dass der Client einen Sec-WebSocket-Key-Header sendet (16 zufällige Bytes, Base64-kodiert), und dass der Server mit Sec-WebSocket-Accept antwortet: dem SHA-1-Hash des Keys, konkateniert mit einer festen magischen GUID, Base64-kodiert. Wenn der Wert nicht passt, schlägt der Handshake fehl, und die Verbindung wird nicht upgegradet. Der ganze Sinn dieser Zeremonie ist, dass ein Server, der nur HTTP spricht, sie nicht versehentlich abschließen kann - die magische GUID existiert, damit die Berechnung absichtlich überkompliziert aussieht. Und da der Browser sowohl das Hashen als auch das Kodieren kann, können Sie die erwartete Antwort selbst berechnen. Das macht das Debuggen von Proxys und Gateways zu einem Einzeiler:

const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
async function expectedAccept (clientKey) {
  const digest = await crypto.subtle.digest(
    'SHA-1',
    new TextEncoder().encode(clientKey + MAGIC)
  );
  return btoa(String.fromCharCode(...new Uint8Array(digest)));
}
const accept = await expectedAccept('dGhlIHNhbXBsZSBub25jZQ==');
console.log(accept); // "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

Die letzte Zeile ist kein Zufall - sie ist das exakte Beispiel aus dem RFC, Byte für Byte wiederholt. Wenn Ihr Gateway mit irgendetwas anderem antwortet, wissen Sie nun genau, welche Seite der Gleichung lügt.

HTTP-Header und Query-Strings

Base64 ist bei HTTP-Headern beliebt, weil Header ASCII sein müssen, und der bekannteste Fall ist die Basic-Authentifizierung: Authorization: Basic gefolgt von der Base64-Kodierung von username:password. So einen Header zu lesen (etwa um anzuzeigen, was eine Anfrage mit sich führt) ist eine Trennung und ein Dekodieren:

const header = 'Basic YWxpY2U6c2VjcmV0MTIz';
const [user, ...rest] = atob(header.slice(6)).split(':');
const password = rest.join(':');
console.log(user, password); // "alice secret123"

Das Spread-und-wieder-Verbinden-Muster behandelt den unangenehmen, aber legalen Fall eines Passworts, das einen Doppelpunkt enthält, denn der Trennpunkt ist immer der erste nach dem Benutzernamen. Dasselbe Muster gilt überall dort, wo ein Header einen strukturierten Wert schmuggelt: Proxy-Authorization, einige herstellerspezifische Header und der gelegentliche Cookie. In Query-Strings und Deep-Links taucht Base64 auf, wenn eine App Zustand teilen will, ohne Server: ein OAuth-state-Wert, ein wiederhergestelltes Suchformular, eine "setze da fort, wo ich war"-Markierung. Dekodieren Sie defensiv - einwickeln in try/catch, denn der Wert hat eine Netz-Grenze überschritten und ihm konnte alles passieren - und behandeln Sie das Ergebnis als nicht vertrauenswürdige Eingabe. Punkt.

Was uns zum Satz führt, der über jedem Terminal hängen sollte: Base64 ist keine Verschlüsselung. Es ist nicht einmal im eigentlichen Sinne eine Verschleierung, denn das "Entschlüsseln" ist ein Funktionsaufruf, den jede Sprache der Erde implementiert. Wenn ein Wert geheim bleiben muss, macht das vorherige Kodieren in Base64 ihn weniger sicher, nicht mehr - es erzeugt die Illusion von Privatsphäre und fügt für jeden, der das Original will, genau einen trivialen Schritt hinzu.

Zustand in der URL und im Speicher

Dieselbe Logik erstreckt sich auf alles, was einen Seiten-Reload oder einen Share-Link überstehen muss. Die üblichen Verdächtigen: localStorage- und sessionStorage-Werte, die strukturierte oder binäre Daten tragen, das Hash-Fragment einer URL für den Routing-Zustand einer Single-Page-App und Konfigurations-Blobs, die Build-Tools in Seiten einbetten. Die Speicher-Geschichte verdient ein konkretes Beispiel, denn die Lese-Seite paart sich mit der Schreib-Seite, die Sie sich merken werden:

const raw = localStorage.getItem('profile');
const profile = JSON.parse(new TextDecoder().decode(base64ToBytes(raw)));

Drei Dinge sind zu beachten. Erstens: Budgets. Browser geben jedem Origin etwa 5 Megabyte localStorage, und Ihr gespeicherter Base64-String frisst etwa 33 Prozent mehr als die Originaldaten. Eine 3,5-Megabyte-Datei wird so still und leise zu 4,6 Megabyte Speicher - und der String lebt als UTF-16 im Speicher, was den Fußabdruck, solange die Seite offen ist, erneut verdoppelt. Zweitens: Konsistenz. Kodieren und dekodieren Sie auf beiden Seiten mit demselben Zeichensatz, sonst speichern Sie tadellose Bytes und lesen Mojibake. Drittens: Share-Links. Wenn der Zustand in der URL reist, verwenden Sie das URL-sichere Alphabet, damit der Wert Copy-Paste übersteht, und halten Sie ihn kurz, denn URL-Längen über ein paar tausend Zeichen machen alte Clients und Logging-Tools nervös.

Wenn die Daten in Stücken ankommen

Manchmal kommt das Base64 nicht als ein einzelner String an: Eine WebSocket-Nachrichtengrenze schneidet es in zwei Hälften, ein Server-Sent-Event-Stream lässt es tröpfeln, ein Chunked-Upload füttert es ein paar Kilobytes am Stück. Sie können atob() nicht auf ein Fragment aufrufen, denn Base64-Gruppen sind 3-Byte-Einheiten, ausgedrückt in 4-Zeichen-Blöcken, und ein Schnitt mitten in einer Gruppe hinterlässt einen hängenden Rest. Der altmodische Fix bestand darin, Zeichen zu puffern, bis man ein Vielfaches von vier hatte, und den Puffer in Scheiben zu dekodieren. Die 2025er-API macht das sauber: Uint8Array.prototype.setFromBase64(string, options) schreibt dekodierte Bytes in ein vorhandenes Array und gibt ein Objekt mit zwei Zahlen zurück: read (wie viele Zeichen es verbraucht hat) und written (wie viele Bytes es erzeugt hat). Mit lastChunkHandling: "stop-before-partial" dekodiert es nur vollständige Gruppen und lässt den unvollständigen Rest ungelesen - genau das Verhalten, das ein Stream-Dekodierer will:

const parts = [];
let carry = '';
for (const piece of incomingPieces) {
  let pending = carry + piece;
  for (;;) {
    const room = new Uint8Array(8);
    const result = room.setFromBase64(pending, {
      lastChunkHandling: 'stop-before-partial'
    });
    parts.push(room.subarray(0, result.written));
    pending = pending.slice(result.read);
    if (result.read === 0) {
      carry = pending;
      break;
    }
  }
}
const size = parts.reduce((sum, part) => sum + part.length, 0);
const bytes = new Uint8Array(size);
let at = 0;
for (const part of parts) {
  bytes.set(part, at);
  at += part.length;
}
const text = new TextDecoder().decode(bytes);

Lesen Sie die innere Schleife langsam, denn sie ist das ganze Muster: Füttern Sie den übernommenen Rest plus das neue Stück hinein, lassen Sie den Dekodierer so viele vollständige Gruppen verbrauchen, wie passen, merken Sie sich, wie viel übrig blieb, indem Sie result.read Zeichen abschneiden, und wenn nichts Vollständiges mehr übrig ist (result.read === 0), legen Sie den Rest als neues carry beiseite und warten Sie auf das nächste Stück. Das Uint8Array(8) ist nur ein Zwischenspeicher - eine Gruppe von vier Zeichen erzeugt höchstens drei Bytes, also ist acht großzügig. Am Ende hält carry, was der Stream nie fertig gestellt hat. Das ist entweder Ihr Fehlsignal oder Ihre "Verbindung endete sauber"-Prüfung.

Wenn Sie Base64 nicht dekodieren sollten

Eine gute Referenz lehrt Sie, wann man das Werkzeug weglegt. Wenn Sie beide Enden des Kanals kontrollieren, greifen Sie stattdessen zu Roh-Bytes: fetch mit response.arrayBuffer() für Downloads, file.arrayBuffer() für Picker-Dateien, ArrayBuffer-Payloads in WebSockets und multipart FormData für Uploads. Keines davon berührt Base64, und Sie bekommen die Daten mit voller Geschwindigkeit, ohne Steuer auf die Größe und ohne String-in-Speicher-Fußabdruck. Base64 zahlt sich genau dann, wenn der Kanal nur Text erlaubt: JSON-Bodies, Query-Strings, E-Mail, Speicher, Legacy-APIs und alles, dessen Vertrag "ASCII or bust" sagt. Ab dem Moment, in dem ein Byte reichen würde, zahlt ein Base64-String eine 33-Prozent-Zulage für das Privileg, druckbar zu sein, und die Zulage wird in Bandbreite, Speicher und CPU eingetrieben - drei Rechnungen, die Sie alle vermeiden können.

Häufige Dekodier-Fallen

Nach all den glatten Pfaden ist hier die Liste, wie einem das in den Fuß fällt, in etwa der Reihenfolge, in der Sie es begegnen wird:

  • Das Ergebnis von atob() als Text zu behandeln. Es ist ein Binärstring. Über TextDecoder wird er zu Text; direkt ausgedruckt wird er zu Mojibake. Diese eine Verwirrung verursacht die meisten "Base64 funktioniert nicht"-Meldungen.
  • Zu erwarten, dass Unicode einfach funktioniert. Die Bytes von "你好" lassen sich gern dekodieren, aber sie sind immer noch Bytes, bis ein Dekodierer Ihnen sagt, dass sie UTF-8 sind. Kodieren und dekodieren Sie auf beiden Seiten im selben Zeichensatz.
  • base64url in atob() zu füttern. Ein einzelnes - oder _ wirft. Wandeln Sie zuerst das Alphabet um, oder verwenden Sie fromBase64 mit der richtigen Option.
  • Zu glauben, jeder lange String sei Base64. Ein gültiger, gepadderter Base64-String hat eine Länge, die ein Vielfaches von vier ist (ungepaddertes base64url darf auf 2 oder 3 enden), und verwendet höchstens ein Alphabet. Eine Länge von eins modulo vier ist sofortiges Scheitern - prüfen Sie es, bevor Sie einen try/catch dafür einsetzen.
  • Padding zu vertrauen, das Sie nicht vereinbart haben. Manche Systeme streichen =, manche behalten es, und manche fügen es mitten in einen umgebrochenen String ein, wo es nicht hingehört. Vereinbaren Sie es mit dem Sender, und entscheiden Sie dann, ob Sie nachsichtig (atob) oder streng (fromBase64) sein wollen.
  • Stille Beschädigung durch einen nachsichtigen Dekodierer. Ein TextDecoder im Standardmodus ersetzt ungültige Bytes durch U+FFFD und sagt nichts. Setzen Sie fatal: true, wenn die Daten wichtig sind.
  • Anzunehmen, Base64 schütze irgendetwas. Tut es nicht. Es ist ein Serialisierungsformat, ein Funktionsaufruf von Klartext entfernt, und "wir Base64-es, damit Nutzer es nicht lesen können" ist eine Sicherheits-Attitüde, keine Kontrolle.
  • Den Speicher zu vergessen. Ein dekodierter Binärstring von einem Megabyte belegt als UTF-16-String zwei Megabyte, während eine Uint8Array derselben Daten eines belegt. Bei großen Payloads: direkt zu fromBase64.
  • Bei jedem Render neu zu dekodieren. Ein paar Megabyte zu dekodieren ist schnell, aber nicht gratis - und es ist nichts, was man einmal pro Frame tun sollte. Dekodieren Sie einmal, cachen Sie die Bytes und rendern Sie aus dem Cache.

Performance-Anmerkungen

Die Kurzfassung: Die nativen Dekodierer sind schnell, und der langsame Teil alten Codes ist meist das JavaScript drumherum, nicht das Base64 selbst. In den Größen, die zählen, sieht es gleich aus: Ein 10-Megabyte-Payload dekodiert Uint8Array.fromBase64 in einstelligen Millisekunden; atob allein ist ein paar Mal langsamer, und die klassische Nachschleife, die Zeichen in ein Byte-Array umwandelt, braucht für dieselbe Eingabe etwa zwanzigmal so lange wie fromBase64. Denn sie führt rund dreizehn Millionen Property-Writes im Haupt-Thread aus. Praktische Folgen: Bevorzugen Sie fromBase64, wo Ihr Publikum es hat; behalten Sie den atob-Helfer, wo es nicht da ist; bauen Sie nie ein Byte-Array, indem Sie Strings in einer Schleife konkatenieren; und wenn Sie einen riesigen Payload verarbeiten müssen, überlegen Sie, die dekodierte Uint8Array einem Web Worker zu geben - die Bytes werden ohne Kopieren übertragen, und der Haupt-Thread bleibt frei, um die UI bei 60 Frames pro Sekunde zu halten. Und denken Sie an die Richtung der Mathematik: Dekodieren schrumpft, also belegt ein dekodierter Buffer immer weniger Speicher als der String, aus dem er kam. Durch Dekodieren können Sie nie den Speicher aufbrauchen; nur dadurch, dass Sie String und Bytes länger als nötig beide um sich haben, können Sie den Speicher aufbrauchen.

Eine kurze Geschichte des Dekodierens in Browsern

Base64 ist älter als die meiste des modernen Webs, aber die Dekodierer der Browser haben eine Geschichte, die es zu kennen lohnt, denn sie erklärt, warum das Ökosystem voller Relikte ist. atob und sein Bruder btoa sind älter als die Spezifikation, die sie heute abdeckt: Der WHATWG-HTML-Standard definierte sie erst im Februar 2011, als ihr langjähriges Browser-Verhalten reverse-engineered in den Standard übernommen wurde. Die Engines hatten sie ohnehin früh ausgeliefert: Firefox seit Version 1 im Jahr 2004, Safari 3, Chrome 4. Internet Explorer übersprang sie komplett bis zu IE 10 im Jahr 2012, deshalb ist JavaScript vor 2012 ein Museum selbst gebastelter Base64: Nachschlagetabellen, String.fromCharCode-Gymnastik und der berüchtigte unescape(encodeURIComponent())-Spruch für Unicode, ein Paar Funktionen, das in der Sprache deprecatet wurde und in Browsern ein Jahrzehnt lang aus reiner Trägheit überlebte. Dann kam die Zeichensatz-Ebene: TextEncoder und TextDecoder aus dem Encoding-Standard kamen zwischen 2013 und 2017 (Firefox 18, Chrome 38, Safari 10.1, und in keinem IE je) und gaben der Plattform endlich einen prinzipienbasierten Weg, Bytes in Wörter zu verwandeln. Node.js, das bis Version 16 im Jahr 2021 nie atob oder btoa als Globals hatte, verbrachte sein früheres Leben mit Buffer und einem Paar kleiner npm-Shims. Und dann schloss sich der Kreis: Firefox 133 (November 2024) und Safari 18.2 (Dezember 2024) lieferten zuerst Uint8Array.fromBase64, toBase64 und die Freunde aus, und die zweite Hälfte von 2025 vollendete das Set, als Chrome 140 (September) und Node 25 (Mitte Oktober) landeten und das Baseline-Programm sie als Newly available markierte. Zum ersten Mal bekam die Sprache selbst - nicht die Web-Plattform - Base64 eingebaut. Ein jahrzehntealtes Format wurde gerade zu einem Standardbibliotheks-Feature der Sprache, und das nächste Jahrzehnt Code darf aufhören, Helfer weiterzukopieren.

Fun-Fakten

  • Der schnellste "Ist das überhaupt Base64?"-Test, der existiert, ist string.length % 4 === 0. Jeder gültige, gepadderte Base64-String besteht ihn; alles andere ist ein Fremder.
  • atob('') gibt '' zurück. Der leere String ist die einzige Eingabe ohne Bytes, und er geht sauber durch die gesamte Pipeline - ein Sonderfall ist nie nötig.
  • Die magische WebSocket-GUID, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, ist ein fester Wert, in den RFC eingebacken, gewählt, damit ein einfacher HTTP-Server den Handshake nie versehentlich abschließen kann. Sie ist die berühmteste Konstante im Protokoll-Engineering, die niemand je erzeugt.
  • Chrome und Firefox werfen für denselben Fehler dieselbe Ausnahme, aber mit unterschiedlichen Meldungen. Fangen Sie anhand von error.name ab, nicht anhand des Meldung-Strings, sonst hat Ihre Fehlerbehandlung einen Browser-Akzent.
  • Ein ein-Megabyte-Binärstring wiegt zwei Megabyte im Speicher, denn JavaScript-Strings sind UTF-16: Jedes dekodierte Byte reist mit einem Byte ungenutzter Reserve mit. Die Uint8Array hat keine solche Steuer.
  • "Data URI" ist ein zurückgedrängter Name. Der WHATWG hat es als Teil der großen URI-zu-URL-Harmonisierung in "data URL" umbenannt. Deshalb begegnen Ihnen beide Schreibweisen in Spezifikationen, Posts und Paketnamen.
  • RFC 4648 liefert eine Tabelle von Testvektoren mit - "f", "fo", "foo", "foob", "fooba", "foobar" und die Freunde, jeder mit seiner bekannten Kodierung -, gegen die Dekodierer-Autoren seit zwanzig Jahren prüfen. Wenn Ihr Dekodierer diese Zeilen besteht, ist er mit fast sicherer Wahrscheinlichkeit korrekt.
  • Der am häufigsten produzierte Base64-String in der Geschichte der Informatik ist mit fast sicherer Wahrscheinlichkeit aGVsbG8=, die Kodierung von "hello". Jedes "Loslegen"-Tutorial, jede Testsuite und jede Stack-Overflow-Antwort auf dem Planeten gibt seine Stimme ab.

Zusammenfassung

So passt das ganze Handwerk des Dekodierens im Browser auf eine Seite: atob() für das schnelle, nachsichtige, universelle Dekodieren; TextDecoder, um die Bytes in die Wörter zu verwandeln, die Sie wirklich wollen, mit fatal: true, wenn die Daten wichtig sind; und Uint8Array.fromBase64 für den modernen, strengen, schnellen Weg, der den String komplett überspringt. Dazwischen haben die Varianten Namen und Regeln: base64url für alles, was in einer URL reist, Padding, das da sein darf oder nicht, und Leerzeichen, die der alte Dekodierer still und leise isst. Und darunter liegen zwei Haltungen: Die Bytes sind nicht der Text, und der Text ist nicht das Geheimnis. Dekodieren Sie mit Absicht, verifizieren Sie, bevor Sie vertrauen, und wenn der Kanal es erlaubt, überspringen Sie Base64 und nehmen Sie die Bytes.

Die andere Hälfte der Reise - Ihre Bytes und Ihren Text nehmen und sie in den druckbaren String verwandeln, mit dem all das begann - ist im Begleit-Leitfaden zum Base64-Kodieren in JavaScript ausführlich behandelt, verlinkt unten.

Zuletzt aktualisiert: 2026-09-08

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