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/Node.js: Ein vollständiger Leitfaden

Ihre Anwendung empfängt eine Base64-Zeichenkette. Sie mag der Authorization-Header einer eingehenden Anfrage sein, ein Feld in einem JSON-Payload, ein Bild, das sich in einer Data-URL versteckt, oder ein Zertifikat, das in eine Konfigurationsdatei geklebt wurde. All das ist dasselbe: rohe Bytes in einem ASCII-Kostüm. Dieser Artikel handelt davon, dieses Kostüm in JavaScript und Node.js auszuziehen, und zwar ohne dabei auch nur ein einziges Byte zu verlieren.

Ein kurzer Hinweis zum Format selbst: Base64 ist eine Textkodierung, die je drei Eingabe-Bytes auf vier druckbare Zeichen abbildet. Die Startseite dieser Site erklärt das Alphabet, die Mathematik und das Padding in voller Detailtiefe, also bleibt es hier bei einem einzigen Satz. Eine Konsequenz lohnt es sich, in der Tasche zu behalten: kodierte Daten sind etwa 33 Prozent größer als die Bytes, die sie tragen. Dekodieren ist also eine schrumpfende Operation, und nichts in diesem Artikel fügt Geheimhaltung hinzu oder nimmt sie wieder weg. Sie entpacken, Sie brechen keine Siegel.

Die gute Nachricht: Sie installieren nichts. Browser liefern atob() seit zwei Jahrzehnten mit, Node.js hat die Buffer-Klasse mit eingebautem base64-Modus, und moderne Runtimes liefern nun Uint8Array.fromBase64(), einen strengen und konfigurierbaren Neuzugang aus der ES2026-Spezifikation. Die Kunst besteht darin, das richtige Werkzeug für den Job auszuwählen und genau zu wissen, was jedes einzelne verzeiht. Denn auf einem Server dekodieren Sie Daten von Fremden, und die Nachsicht ist es, bei der die Dinge schiefgehen.

Einen Decoder wählen

Drei APIs decken den Großteil aller Dekodierarbeit ab. Sie unterscheiden sich im Temperament, und dieser Unterschied ist die ganze Geschichte:

Decoder Verfügbar in Temperament
Buffer.from(string, 'base64') Node.js (jede Version, die zählt) nachsichtig: überspringt unbekannte Zeichen, stoppt beim ersten =, wirft nie etwas aus
atob(string) alle Browser, Node.js 16 und neuer streng: wirft bei fehlerhafter Eingabe InvalidCharacterError, überspringt ASCII-Leerraum, verzeiht fehlendes Padding
Uint8Array.fromBase64(string) Chrome 140+, Firefox 133+, Safari 18.2+, Node.js 25+ konfigurierbar: Sie wählen das Alphabet und wie streng der letzte Chunk sein muss

Alle drei öffnen denselben klassischen Payload auf dieselbe Weise:

// Das Arbeitstier von Node.js
const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVsbG8gd29ybGQ=', 'base64').toString('utf8')); // "hello world"
// Das klassische Duo (jeder Browser, Node.js 16+)
console.log(atob('aGVsbG8gd29ybGQ=')); // "hello world", als Binär-String
// Die moderne ES2026-Methode (Chrome 140+, Node.js 25+)
console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8gd29ybGQ='))); // "hello world"

Eine Warnung, bevor Sie sich auf atob() verlassen: Sie gibt einen String zurück, aber einen Binär-String, eine Zeichenkette, in der jedes Zeichen genau ein rohes Byte als Codepunkt von 0 bis 255 trägt. Ausdrucken können Sie ihn ohne Weiteres. Speichern Sie ihn in JSON, einer Datenbank oder einem Cookie, reisen diese rohen Byte-Werte mit, also wandeln Sie ihn sofort nach dem Dekodieren in echte Bytes oder echten Text um.

Der nachsichtige Decoder und was er verschluckt

Der Buffer von Node ist ein nachsichtiger Leser, und das ist ein zweischneidiges Schwert. Für Daten, die raue Straßen zurückgelegt haben, ist er herrlich: MIME-E-Mails mit ihren Zeilenumbrüchen, per Hand kopierte Zeichenketten, Log-Ausgaben mit versehentlichen Leerzeichen. Gefährlich wird er bei Daten, die Sie nicht selbst erzeugt haben, denn er beschwert sich nie. Hier ist, was wirklich passiert:

Eingabe Was Buffer.from(input, 'base64') tut
'!!!' Gibt einen leeren Buffer zurück. Der gesamte Müll wird übersprungen, nichts wird dekodiert, kein Fehler.
'aGVsbG8== garbage' Gibt "hello" zurück. Das erste = beendet das Dekodieren; der Rest wird ignoriert.
'aG!VsbG8' Gibt "hello" zurück. Das Ausrufezeichen wird übersprungen, kein Fehler.
'aGVs=bG8' Gibt "hel" zurück. Ein = mitten in der Zeichenkette beendet die Show zu früh.
'aGVsbG8====' Gibt "hello" zurück. Überflüssiges Padding am Ende wird ignoriert.
'=aGVsbG8' Gibt einen leeren Buffer zurück. Padding vor den Daten bedeutet nichts.

Das Gegenmittel für nicht vertrauenswürdige Eingabe ist ein Validator, und die Grammatik von Base64 ist klein genug, um in einen einzigen regulären Ausdruck zu passen:

const STRICT = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
function decodeStrict (base64) {
  if (!STRICT.test(base64)) {
    throw new TypeError('Not a valid base64 string');
  }
  return Buffer.from(base64, 'base64');
}
console.log(decodeStrict('aGVsbG8gd29ybGQ=').toString('utf8')); // "hello world"
try {
  decodeStrict('aGVs!bG8');
} catch (error) {
  console.log(error.message); // "Not a valid base64 string"
}

Der reguläre Ausdruck prüft die Form: Vierergruppen mit korrektem Padding. Eine Regel, die er nicht prüfen kann, ist die Regel für die kanonische Kodierung aus RFC 4648, nach der die ungenutzten Pad-Bits der letzten Gruppe null sein müssen. Der strikte Modus von Uint8Array.fromBase64() prüft genau das, und so können Sie auf Node.js 25 oder in jedem modernen Browser den regulären Ausdruck ganz weglassen und die Plattform die Prüfung erledigen lassen:

console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8'))); // "hello", der loose-Modus verzeiht fehlendes Padding
try {
  Uint8Array.fromBase64('QQB=', { lastChunkHandling: 'strict' });
} catch (error) {
  console.log(error.name); // "SyntaxError", die Pad-Bits sind nicht null
}

Die Option lastChunkHandling hat drei Einstellungen, die sich zu kennen lohnen. "loose" (der Standard) überspringt Leerraum, akzeptiert fehlendes Padding und ignoriert übrig gebliebene Pad-Bits. "strict" verlangt eine vollständige, gepaddete letzte Gruppe mit allen Pad-Bits auf null gesetzt. Und "stop-before-partial" dekodiert nur vollständige Gruppen zu vier Zeichen und überlässt das nachhängende Fragment Ihnen, damit Sie es mitnehmen. Genau dieser Teil macht das Streaming-Dekodieren angenehm, wie Sie später in diesem Artikel sehen werden.

Von Bytes zu Text: Die Zeichensatz-Entscheidung

Das Dekodieren von Base64 liefert Ihnen Bytes. Bytes werden erst dann zu Text, wenn Sie einen Zeichensatz wählen, und diese Wahl steht Ihnen frei, in der Regel basierend darauf, was der Sender versprochen hat. Die Standardeinstellung von Node ist die, die Sie die meiste Zeit brauchen:

const { Buffer } = require('node:buffer');
const bytes = Buffer.from('w6k=', 'base64'); // die zwei Bytes C3 A9
console.log(bytes.toString('utf8'));   // "é", die zwei Bytes verschmelzen zu einem Zeichen
console.log(bytes.toString('latin1')); // "é", dieselben Bytes, ein Zeichen nach dem anderen gelesen

Bei UTF-8 hat es einen Haken: Wenn eine Byte-Folge kein gültiges UTF-8 ist, wirft Node keinen Fehler aus. Er ersetzt sie durch das Unicode-Ersatzzeichen (U+FFFD, die Raute mit Fragezeichen) und macht einfach weiter. Das heißt, ein beschädigter Payload kann Ihre Pipeline durchsegeln und in Ihrer Datenbank landen. Der echte Text-Dekoder der Plattform, TextDecoder (ein Global in Node.js und jedem Browser), hat eine fatal-Option, die Beschädigungen in eine fangbare TypeError verwandelt:

const stray = new Uint8Array([0xe9]); // ein einzelnes Byte, kein gültiges UTF-8
console.log(new TextDecoder().decode(stray)); // das Ersatzzeichen, kein Fehler
try {
  new TextDecoder('utf-8', { fatal: true }).decode(stray);
} catch (error) {
  console.log(error.name); // "TypeError"
}

Legacy-Systeme sterben nie, und TextDecoder weiß immer noch, wie man sie liest. Er akzeptiert die vollständige Label-Tabelle des WHATWG-Encoding-Standards, sodass sich ein Base64-Payload aus einer Windows-Anwendung der 1990er, einem japanischen Mainframe oder einem alten FTP-Spiegel weiterhin mit Labels wie 'windows-1250', 'shift_jis', 'euc-kr' oder 'gb18030' dekodieren lässt, alle case-insensitiv. Ein Label verdient eine Warnung, denn es hat schon echte Debugging-Zeit gekostet: Die Spezifikation alias 'iso-8859-1', 'latin1' und sogar 'us-ascii' auf den Windows-1252-Dekoder. Byte 0x80, ein Steuerzeichen in echtem Latin-1, kommt als Euro-Symbol heraus:

console.log(new TextDecoder('iso-8859-1').decode(new Uint8Array([0x80]))); // "€", nicht das Latin-1, das Sie verlangt haben
// Für eine echte Byte-für-Byte-Lesung in Latin-1 die Buffer-Seite nutzen:
console.log(Buffer.from('gA==', 'base64').toString('latin1')); // das rohe Steuerzeichen 0x80

Wenn Sie wirklich diese rohe Abbildung brauchen, überträgt 'latin1' von Buffer (deren Legacy-Alias 'binary' ist, und der in den Worten der Node-Dokumentation ein sehr irreführender Name ist) Byte N auf Codepunkt N, ohne den Umweg über Windows. Für alles Moderne ist UTF-8 plus fatal: true das sichere Paar.

Ein JWT aufbrechen

Der mit Abstand häufigste Base64-Payload, den ein JavaScript-Service dekodiert, ist ein JSON Web Token, die xxxxx.yyyyy.zzzzz-Zeichenkette, die im Authorization-Header von einem halben Web mitfährt. Nach RFC 7515 besteht ein kompaktes JWS aus drei durch Punkte getrennten Teilen, und die ersten beiden sind JSON-Objekte, kodiert als base64url ohne Padding. In Node.js zu lesen, macht keine Umstände, denn der base64url-Modus ist eine erstklassige Kodierung:

const { Buffer } = require('node:buffer');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIn0.JMjpmDdNzQZpTuUO1H33GJsj7nWhBu-qxkPD0GL2uaA';
const [head, body, signature] = token.split('.');
console.log(JSON.parse(Buffer.from(head, 'base64url').toString('utf8'))); // { alg: 'HS256', typ: 'JWT' }
console.log(JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))); // { sub: '123', name: 'Ada' }

Oft genug gesagt, aber es lohnt sich, es zu wiederholen: Dekodieren ist keine Verifizierung. Header und Payload sind nur verkleidet, nicht verschlüsselt, und jeder, der das Token hält, kann beide lesen. Der Teil, den Sie prüfen müssen, ist der dritte: die Signatur. Für ein klassisches HMAC-SHA256-Token ist der gesamte Check nur wenige Zeilen des eingebauten crypto-Moduls, und der einzige kniffelige Teil ist der Vergleich mit timingSafeEqual, damit ein Angreifer Ihren Byte-für-Byte-Vergleich nicht per Timing angreifen kann:

const crypto = require('node:crypto');
const expected = crypto.createHmac('sha256', 'topsecret').update(head + '.' + body).digest();
const actual = Buffer.from(signature, 'base64url');
console.log(crypto.timingSafeEqual(expected, actual)); // true
console.log(crypto.timingSafeEqual(crypto.createHmac('sha256', 'wrong-secret').update(head + '.' + body).digest(), actual)); // false

In einem echten Service bauen Sie das in der Regel nicht von Hand. Das Paket jose (ohne Abhängigkeiten, läuft in Node.js, Browsern und Edge-Runtimes) und das langjährige jsonwebtoken-Paket (Node.js) verpacken den Tanz, behandeln die Algorithmus-Familien von RSA und ECDSA und erzwingen die Claims exp, aud und iss. Welches Paket Sie auch wählen, die Base64-Grundarbeit, die darunter liegt, ist dieselbe: die zwei Aufrufe, die Sie gerade gesehen haben.

HTTP: Header, Query-Strings und Cookies

Drei Ecken des Drahtes sind voller Base64. Die älteste ist die HTTP-Basic-Authentifizierung, definiert in RFC 7617: Der Client sendet Authorization: Basic plus das Base64 von user-id:password. Auf dem Server braucht es nur einen Slice und ein Dekodieren, mit der kleinen Protokoll-Finesse, dass nur der erste Doppelpunkt Benutzername und Passwort trennt. Ein Passwort darf also legal weitere Doppelpunkte enthalten, ein Benutzername nicht:

const { Buffer } = require('node:buffer');
const header = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
const credentials = Buffer.from(header.slice(6), 'base64').toString('utf8');
const [user, ...rest] = credentials.split(':');
console.log(user, rest.join(':')); // "Aladdin" "open sesame"

Und denken Sie daran, was Basic-Authentifizierung eigentlich ist: Obskurierung, keine Sicherheit. Die Zugangsdaten reisen im Kostüm über den Draht, weshalb das Verfahren nur über HTTPS akzeptabel ist. Die zweite Ecke ist der Query-String, und er versteckt die unangenehmste Landmine in der Base64-Welt:

const params = new URLSearchParams('token=aGVs+bG8=');
console.log(params.get('token')); // "aGVs bG8=", das Plus wurde zu einem Leerzeichen

Ihr Base64 hat sich nicht selbst beschädigt. Die URL-Ebene hat es höflicherweise im Auftrag der form-encoding-Regeln getan, die + als Leerzeichen behandeln. Genau deshalb verwenden Tokens, die in Query-Strings leben, das URL-sichere Alphabet, behandelt im Abschnitt unten. Die dritte Ecke ist der Cookie: Cookies sind ausschließlich ASCII, also ist jeder nicht-ASCII-Wert, der in einem gespeichert wird, fast sicher Base64, und das alte Muster, einen JSON-Blob per Base64 in einen Cookie zu legen, lebt in erstaunlich vielen Produktionssystemen. Das Dekodieren ist dasselbe, das Sie schon kennen. Prüfen Sie aber zuerst die Form, denn ein Cookie ist genau der Ort, an dem Ihnen ein Benutzer oder ein Browser-Extension Müll übergeben kann.

Dateien, Bilder und Data-URLs

Das Dateisystem von Node spricht Base64 direkt, sodass eine ganze Datei in einer Zeile eine JSON-Grenze überschreiten kann:

const fs = require('node:fs');
const base64 = fs.readFileSync('./photo.png', 'base64');
console.log(base64.length); // die Datei, etwa 33 Prozent schwerer
const bytes = Buffer.from(base64, 'base64');
fs.writeFileSync('./photo.copy.png', bytes);

Der andere dateiförmige Payload ist die Data-URL, die data:image/png;base64,...-Zeichenkette, die Frontends für Inline-Bilder lieben. Das Rezept ist in jeder Runtime dasselbe: beim ersten Komma schneiden, die Metadaten davor parsen, den Rest dekodieren. Hier ist ein echter PNG aus einem einzigen Pixel, der wieder zum Leben erwacht:

const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const comma = dataUrl.indexOf(',');
const meta = dataUrl.slice(5, comma);
const bytes = Buffer.from(dataUrl.slice(comma + 1), 'base64');
console.log(meta); // "image/png;base64"
console.log(bytes.subarray(0, 8).toString('hex')); // "89504e470d0a1a0a", die PNG-Signatur

Die Signatur zu prüfen ist eine günstige Gewohnheit. Die ersten acht Bytes eines PNG sind immer 89 50 4E 47 0D 0A 1A 0A, und ein JPEG beginnt mit FF D8 FF. Wenn ein "Base64-Bild" von einem Client nicht mit den versprochenen magischen Bytes beginnt, wissen Sie es jetzt, bevor Sie irgendetwas Aufwandvolles damit machen.

URL-sicheres Base64: Das Alphabet für Tokens

Klassisches Base64 verwendet + und / als seine zwei Sonderzeichen (RFC 4648, Abschnitt 4), und beide sind in URLs lästig: + wird beim form-dekodieren zu einem Leerzeichen, und / ist ein Pfadtrenner. Die URL- und Dateinamen-sichere Variante aus Abschnitt 5, die alle base64url nennen, tauscht sie gegen - und _ aus und kann das =-Padding am Ende komplett fallen lassen, wenn die Länge aus dem Kontext bekannt ist. Genau diese Kombination brauchen JWTs, OAuth-Tokens und Deep Links, und so ist base64url das Alphabet, auf das Sie in der Praxis am häufigsten stoßen werden.

Der Buffer von Node macht daraus eine Kleinigkeit. Sowohl der 'base64'- als auch der 'base64url'-Dekodiermodus akzeptieren alle vier Sonderzeichen und ordnen sie denselben Werten zu. So dekodieren ein JWT-Teil, ein OAuth-Token und ein klassischer Base64-Blob alle ohne jeden Zeichen-Tausch-Umweg:

const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVs-bG8', 'base64').toString('hex'));    // "68656cf9b1bc"
console.log(Buffer.from('aGVs+bG8', 'base64url').toString('hex')); // "68656cf9b1bc", exakt dieselben sechs Bytes

Die ES2026-API ist mit Absicht wählerischer, und sie gibt Ihnen dieselbe Flexibilität mit einem expliziten Regler. Die Option alphabet wählt zwischen "base64" (der Standard, + und /) und "base64url" (- und _), und ein Zeichen aus dem falschen Alphabet ist eine SyntaxError, kein stiller Cross-Alphabet-Decode:

console.log(Uint8Array.fromBase64('aGVs-bG8', { alphabet: 'base64url' }).length); // 6
try {
  Uint8Array.fromBase64('aGVs-bG8'); // das Standard-Alphabet ist das klassische
} catch (error) {
  console.log(error.name); // "SyntaxError", der Bindestrich ist kein klassisches Zeichen
}

In einem Browser, der die neuen Methoden noch nicht hat, ist der Umweg ein kleiner Tausch, bevor die Zeichenkette an atob() übergeben wird, das nur das klassische Alphabet kennt. Sie müssen auch das Padding wiederherstellen, falls es der Sender weggelassen hat, was bei Token-artigen Payloads die Norm ist:

function decodeBase64Url (value) {
  const classic = value.replace(/-/g, '+').replace(/_/g, '/');
  const padded = classic + '='.repeat((4 - (classic.length % 4)) % 4);
  const binary = atob(padded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}
console.log(new TextDecoder().decode(decodeBase64Url('aGVsbG8gd29ybGQ'))); // "hello world"

Base64 in freier Wildbahn: Wo sich Payloads verstecken

Base64 ist der Postdienst für Bytes in der JavaScript-Welt. Eine Tour durch die Orte, an denen es auftaucht, mit dem Dekodier-Rezept für jede Station:

  • JSON-API-Felder, bei Weitem der häufigste Träger: Avatare, Thumbnails, generierte Dokumente und Uploads kommen als Base64-Zeichenketten in ganz gewöhnlichem JSON an, weil JSON kein Wort für "das sind Bytes" hat. Dekodieren Sie das Feld, bevor Sie irgendetwas anderes damit machen.
  • Umgebungsvariablen und Konfigurationsdateien: mehrere Secret-Manager, CI-Systeme und die npm-CLI selbst übergeben Ihnen Base64-Blobs (ältere npm-Versionen speicherten die Registry-Anmeldeinformationen in .npmrc als Base64 von user:password; modernes npm schreibt ein rohes Bearer-Token in _authToken). Dekodieren Sie einmal beim Start, und halten Sie den Klartext nur so lange im Speicher, wie Sie ihn brauchen.
  • Kubernetes und Cluster-Tools: k8s-Secrets sind berühmt-berüchtigt als Base64-kodiert in der API und in etcd, und die offizielle Dokumentation wiederholt ständig, dass es Kodierung ist, keine Verschlüsselung. Ihr Dekodier-Code sollte das Ergebnis als Secret behandeln, nicht als Beweis für Sicherheit.
  • Datenbanken: alles Binäre, das in einer JSON-Spalte gespeichert wird (Postgres jsonb, MongoDB-Dokumente, Redis), ist häufig eine Base64-Zeichenkette. Dekodieren Sie es im Lese-Pfad in einen Buffer oder eine Uint8Array, und lassen Sie die Datenbank rein textuell.
  • E-Mail: MIME-Base64 mit seinem Zeilenumbruch bei 76 Zeichen ist der Weg, wie Anhänge und binäre Header SMTP durchqueren, ein Protokoll, das ursprünglich nur 7-Bit war. Der Decoder von Node überspringt die Zeilenumbrüche für Sie, sodass der gesamte Körper in einem einzigen Aufruf dekodiert wird, ohne Aufräumen.
  • CI- und CD-Pipelines: Build-Systeme und Secret-Injektoren übergeben Tokens als Base64-Umgebungsvariablen; dekodieren Sie im Pipeline-Skript, und geben Sie den dekodierten Wert nie in ein Log aus.
  • Verzeichnis- und SAML-Daten: LDIF-Dateien speichern binäre Attribute (denken Sie: Zertifikate) als Base64, und SAML-Antworten werden oft zunächst deflated und dann Base64-kodiert, bevor sie eine HTTP-Grenze überschreiten.
  • Worker-Threads und Edge-Runtimes: Base64-Zeichenketten überschreiten die worker_threads-Grenze als gewöhnliche, per structured clone kopierbare Zeichenketten, sodass ein schweres Dekodieren auf einem Worker leben kann, während die Event-Loop des Haupt-Threads frei bleibt.

Zwei dieser Stationen verdienen einen genaueren Blick, denn sie tauchen sowohl in Interviews als auch in der Produktion auf:

const { Buffer } = require('node:buffer');
// Umgebungsvariable: das Secret kommt Base64-kodiert an
const token = Buffer.from(process.env.REGISTRY_TOKEN_B64, 'base64').toString('utf8');
// JSON-API-Feld: erst auspacken, dann weiterarbeiten
const body = { attachment: 'iVBORw0KGgo...' };
const imageBytes = Buffer.from(body.attachment, 'base64');
console.log(imageBytes.subarray(0, 4).toString('hex')); // "89504e47", erneut die PNG-Signatur
// MIME-E-Mail: die Zeilenumbrüche werden übersprungen, kein Aufräumen nötig
const mimeBody = 'SGVsbG8sIHdyYXBw\nZWQgYmFzZTY0IQ==';
console.log(Buffer.from(mimeBody, 'base64').toString('utf8')); // "Hello, wrapped base64!"

Das Anti-Muster, das Sie auf dieser Tour erkennen sollten, ist überall dasselbe: Base64 an einem Ort, an dem rohe Bytes ohnehin erlaubt waren. Ein WebSocket-Frame, ein Dateistream, eine Postgres-bytea-Spalte, all das trägt Bytes nativ, und ein Base64-Roundtrip dort ist reiner Overhead, die 33-Prozent-Größesteuer ohne jeden Nutzen. Wenn es einen nativen Binärpfad gibt, nehmen Sie ihn.

In Stücken dekodieren: Streams und große Daten

Base64 kodiert je vier Zeichen drei Bytes, sodass ein Stream aus Chunks eine Gruppe in zwei Hälften zerreißen kann. Der naive Ansatz, jeden Chunk zu dekodieren und zu beten, macht die Ausgabe an zufälligen Grenzen kaputt. Die ES2026-API wurde genau dafür entworfen: setFromBase64() schreibt in ein vorreserviertes Array und meldet, wie viele Eingabe-Zeichen es verbraucht hat, und der "stop-before-partial"-Modus lässt es bei der letzten vollständigen Gruppe stoppen, sodass das Fragment für den nächsten Chunk übrig bleibt. Das Muster spiegelt die TextDecoder-Stream-API:

const { Buffer } = require('node:buffer');
const chunks = ['aGVsbG8', 'gd29ybGQ='];
let leftover = '';
const parts = [];
for (const chunk of chunks) {
  const pending = leftover + chunk;
  const space = new Uint8Array(Math.ceil(pending.length * 3 / 4));
  const { read, written } = space.setFromBase64(pending, { lastChunkHandling: 'stop-before-partial' });
  parts.push(Buffer.from(space.buffer, space.byteOffset, written));
  leftover = pending.slice(read);
}
parts.push(Buffer.from(Uint8Array.fromBase64(leftover)));
console.log(Buffer.concat(parts).toString('utf8')); // "hello world"

Auf Runtimes ohne die neuen Methoden (und die LTS-Linie von Node hatte sie eine ganze Weile nicht) funktioniert derselbe Loop mit einem kleinen Userland-Dekoder, der eine unvollständige Gruppe trackt, oder Sie puffern einfach die eingehenden Chunks, bis Sie an den Gruppengrenzen teilen können. Die wichtige Idee ist das Mitnehmen: Dekodieren Sie nie ein Fragment auf eigene Faust.

Große Payloads rufen zwei weitere Grenzen auf den Plan. Erstens die Zeichenkette selbst: buffer.constants.MAX_STRING_LENGTH von Node liegt bei 536870888 Zeichen, rund 512 MiB Text, die zu etwa 400 MB Bytes dekodieren. Eine Base64-Datei, die größer ist, braucht einen Streaming-Ansatz und nicht ein einzelnes readFileSync. Zweitens der Speicher: Die kodierte Zeichenkette lebt im JavaScript-Heap als UTF-16, zwei Bytes pro Zeichen, und der Buffer nach dem Dekodieren ist eine zweite Kopie der Daten. Bei großen Payloads halten Sie beide kurzzeitig gleichzeitig, also halten Sie die kodierte Form so kurz wie der Code es erlaubt und bevorzugen Sie Streams für alles, was dateigröße hat.

Aus dem Terminal

Node dient auch als ein vollkommen brauchbarer Base64-Dekoder für die Kommandozeile, was praktisch ist, wenn Sie eine Anfrage debuggen oder einen Config-Wert inspizieren:

# Eine klassische Base64-Zeichenkette als Argument dekodieren
node -e 'console.log(Buffer.from(process.argv[1], "base64").toString("utf8"))' "aGVsbG8gd29ybGQ="
# Die URL-sichere Variante, Padding optional
node -e 'console.log(Buffer.from(process.argv[1], "base64url").toString("utf8"))' "aGVsbG8gd29ybGQ"
# Von stdin dekodieren, wofür Pipes da sind
echo -n "aGVsbG8gd29ybGQ=" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(Buffer.from(d.trim(),"base64").toString("utf8")))'

Alle drei geben hello world aus. Wenn Sie auf dem Rechner auch das klassische base64-Kommando aus coreutils haben, erledigt es denselben Job mit base64 -d, aber die Node-Varianten kennen base64url, was das traditionelle Tool nicht kann.

Stolperfallen mit JavaScript-Akzent

Jede einzelne davon hat schon jemandem einen Nachmittag in JavaScript oder Node.js gekostet:

  • Der stille Decoder: Buffer.from('!!!', 'base64') gibt einen leeren Buffer zurück, keinen Fehler. Halb beschädigte Eingabe dekodiert zu halb beschädigten Daten, ohne jede Warnung. Validieren Sie nicht vertrauenswürdige Eingabe mit dem strikten regulären Ausdruck (oder dem strikten fromBase64-Modus), und behandeln Sie einen leeren Buffer aus einer nicht leeren Zeichenkette als Warnsignal.
  • Das fehlende Encoding-Argument: Buffer.from('aGVsbG8=') ohne zweites Argument dekodiert nichts. Er baut einen Buffer aus den UTF-8-Bytes dieser Buchstaben, und Ihr "dekodiertes" Ergebnis sind die Buchstaben selbst, als Bytes verpackt. Das 'base64'-Argument ist der gesamte Trick.
  • Das Binär-String-Kostüm: Die Ausgabe von atob() ist erst dann Text, wenn Sie es sagen. Wenn Sie sie in eine JSON-Antwort, einen Cookie oder eine Log-Zeile stopfen, "funktioniert" es, und sie bewahrt dabei jedes Null-Byte, was Log-Shippers und Serializer gleichermaßen überrascht. Wandeln Sie sie sofort mit charCodeAt() in eine Uint8Array oder in UTF-8-Text um.
  • Das Plus im Query-String: Ein + in einem form-dekodierten Query-Wert ist bei der Übergabe durch URLSearchParams schon ein Leerzeichen. Bevorzugen Sie base64url für alles, was in einer URL lebt, und kleben Sie niemals ein klassisches Base64-Token unverschleiert in einen Query-String.
  • Das Ersatzzeichen: Ungültiges UTF-8 wird im UTF-8-Modus von Buffer zu einem stillen Raute-Fragezeichen statt zu einem Fehler, sodass ein beschädigter Payload Ihre Pipeline passieren und in einer Datenbank landen kann. Aktivieren Sie fatal: true bei TextDecoder, wo Beschädigung ein lautes Scheitern sein sollte.
  • Der Windows-Umweg: Fragen Sie TextDecoder nach 'iso-8859-1' oder 'latin1', und Sie bekommen den Windows-1252-Dekoder, bei dem Byte 0x80 zum Euro-Symbol wird. Für echtes Latin-1, Byte für Byte, lesen Sie stattdessen den Buffer mit toString('latin1'). Und denken Sie daran, dass 'binary' nur ein irreführender Alias für dieselbe Latin-1-Abbildung ist.
  • Die Größen-Obergrenzen: buffer.constants.MAX_LENGTH sind auf 64-Bit-Systemen 9007199254740991 Bytes (2 hoch 53, minus 1), aber die Zeichenkette, die das Base64 trägt, kann nicht über MAX_STRING_LENGTH von 536870888 Zeichen hinauswachsen. Eine einzelne Zeichenkette kann daher nur etwas über 400 MB dekodiertes Daten tragen; darüber hinaus streamen Sie.
  • Die Speicher-Rechnung: Eine Base64-Zeichenkette kostet zwei Heap-Bytes pro Zeichen (UTF-16), und der Buffer nach dem Dekodieren ist eine vollständige zweite Kopie. Eine 100-MB-Datei wird kurzzeitig zu etwa 133 MB Zeichenkette plus 100 MB Buffer in Ihrem Prozess. Verkleinern Sie das Zeitfenster, in dem die kodierte Form referenziert bleibt.
  • Die Fehlanpassung zwischen strikt-fern und nachsichtig-lokal: Ihr Node-Decoder verzeiht, was ein strenger Decoder woanders verwirft (ein Python-Skript, ein Go-Service, eine mobile App). Wenn eine Seite Ihres Systems streng ist und die andere nachsichtig, taucht der Bug nur bei bestimmten Payload-Längen auf, und das ist die schlechteste Sorte Bug. Vereinbaren Sie die Strenge auf Protokollebene, nicht in Ihrem Kopf.

Wie JavaScript seine Decoder bekam

Die Browser-Seite hat eine lange, langweilige, verlässliche Geschichte. atob() und btoa() wurden Anfang 2011 im HTML5-Entwurf spezifiziert (die Browser hatten sie, bevor die Spezifikation es tat), und sie saßen seitdem in jedem großen Browser, über ein Jahrzehnt lang unverändert im Verhalten. Sie sind älter als die Typed Arrays im Sprachstandard (ES2015), deshalb sprechen sie von "Binär-Strings" statt von Bytes.

Node.js wuchs seinen Decoder auf einer anderen Zeitleiste. Die Buffer-Klasse wurde in Version 0.1.103, im Sommer 2010, fast fünf Jahre vor Node 1.0, zu einem Global, und sie trug den 'base64'-Modus von Anfang an mit sich. Für den Großteil des Lebens von Node war das der einzige Decoder in der Stadt. Dann kam die Welle der Web-Standards: Node 16 im Jahr 2021 fügte atob() und btoa() als Globals hinzu, damit Code, der für den Browser geschrieben wurde, auf dem Server ohne Polyfill läuft, und markierte beide von Tag eins an als Legacy. Node 25, veröffentlicht am 15. Oktober 2025, upgradete V8 auf 14.1 und brachte die ES2026-Methoden, Uint8Array.fromBase64(), setFromBase64() und ihre hex-Brüder, in die Runtime. Auf dem Weg wurde der alte new Buffer()-Konstruktor als deprecated markiert (Node 10 startete die Warnungen 2018), zugunsten von Buffer.from(), alloc() und allocUnsafe(), zum Teil, weil eine nicht initialisierte Allokation den Speicherinhalt hätte undichten können, der zuvor dort lag.

In den Browsern landete dieselbe Welle etwas früher: Firefox 133 und Safari 18.2 lieferten die neuen Methoden 2024, und Chrome 140 (stabil am 2. September 2025) vollendete das Set, woraufhin die Funktion im Baseline-Programm der Browser-Hersteller als Baseline Newly available erklärt wurde. Bun, die All-in-One-JavaScript-Runtime, bekam sie in Version 1.1.22 im August 2024. Und wenn Sie keine aktuelle Runtime verlangen können, liefern core-js und das Paket es-arraybuffer-base64 vom es-shims-Projekt Polyfills für all das, was auch der Weg ist, den die meisten Frameworks intern gehen.

Das Format, das sie bedienen, hat eine noch ältere Genealogie. Das Alphabet wurde 1987 erstmals für Privacy-Enhanced Mail standardisiert (RFC 989), die Revision von 1993 (RFC 1421) behielt dasselbe Alphabet bei, und MIME übernahm es 1996 (RFC 2045), etwa drei Jahre nach dieser Revision, mit seinem Zeilenumbruch bei 76 Zeichen; RFC 3548 im Jahr 2003 konsolidierte base16, base32 und base64 in einem Dokument, und RFC 4648 im Jahr 2006 gab es neu heraus und behielt das URL-sichere Alphabet, das RFC 3548 hinzugefügt hatte - dasselbe, das ein Jahrzehnt später in jedem JWT landen würde. Die URL-sichere Variante ist ein schönes Stück Trivia: Sie wurde in einem Mailing-List-Posting aus dem Jahr 2001 über Peer-to-Peer-Identifikatoren vorgeschlagen, noch bevor sie auf ein Token traf.

Spaßfakten für Ihr nächstes Standup

  • Der Beispiel-Key aus dem WebSocket-RFC, dGhlIHNhbXBsZSBub25jZQ==, dekodiert zu den Worten "the sample nonce". Das Standardisierungsgremium versteckte ein Augenzwinkern in seinem eigenen Beispiel, und atob() von Node knackt den Witz in einem einzigen Aufruf.
  • Buffer.from('!!!', 'base64') gibt einen Buffer der Länge null zurück. Eine echte Allokation mit nichts drin. Nichts. Das kommt Node am nächsten an ein Achselzucken heran.
  • Die Base64-Dekoder von Node sind auf eine Weise zweisprachig, die die Spezifikation nie verlangt hat: +, -, / und _ sind in beiden Modi, 'base64' und 'base64url', willkommen, und jedes Paar bildet denselben Wert ab.
  • Die Node-Dokumentation zu atob() enthält den Satz "Nutzen Sie stattdessen Buffer.from(data, 'base64')". Eine Runtime, die Ihnen sagt, aufzuhören, einen ihrer eigenen Globals zu verwenden, komplett mit einem offiziellen Codemod (npx codemod@latest @nodejs/buffer-atob-btoa), der die Migration für Sie erledigt.
  • Kleine Buffer werden aus einem gemeinsamen Slab herausgeschnitten: Buffer.poolSize sind 65536 Bytes, und jede kleine Allokation wiederverwendet Stücke dieses Pools. Deshalb ist das Erzeugen von Buffern schnell, und deshalb ist "unsafe"-Allokation ein Ausdruck, dessen Bedeutung Sie kennen sollten.
  • Das kleine Paket base64-js, drei Funktionen und null Abhängigkeiten, zieht auf npm weit über 100 Millionen Downloads pro Woche an, fast alles als versteckte Abhängigkeit innerhalb anderer Pakete. Base64 ist der am meisten geschmuggelte Code im Ökosystem.
  • Uint8Array.fromBase64() hat einen Modus namens "stop-before-partial", der existiert einzig und allein, damit Sie einen Stream dekodieren können, ohne jemals eine Vier-Zeichen-Gruppe zu zerreißen. Ein Modus, der nach dem benannt ist, was er sich weigert zu tun, ist ein seltenes Stück API-Poesie.
  • Die Unix-Passwort-Welt verwendet ihre eigenen Base64-flavorierten Alphabete, ohne Padding, und, verwirrenderweise, sind sie nicht alle in derselben Reihenfolge. Das klassische crypt(3)-"hash64"-Alphabet ist ./0-9A-Za-z, aber bcrypt mischt dieselben 64 Zeichen stattdessen zu ./A-Za-z0-9. Die bcrypt-Version begegnen Sie in den $2b$-Hashes, die viele JavaScript-Projekte für Benutzer-Passwörter speichern, und sie ist der Grund, warum "Base64" in einem Sicherheits-Kontext mehrere verschiedene Alphabete bedeuten kann, nicht nur zwei.

Noch eine Richtung offen

Base64 in JavaScript und Node.js zu dekodieren ist ein Stapel aus drei ehrlichen Werkzeugen: Buffer.from(string, 'base64'), das nachsichtige Arbeitstier, das beide Alphabete akzeptiert und jedes irrtümliche Zeichen überspringt, am besten bewacht von einem strikten regulären Ausdruck; TextDecoder, für echten Text in jedem Zeichensatz, den das alte Web je erfunden hat, mit fatal-Modus, wenn Beschädigung wehtun sollte; und das neue Uint8Array.fromBase64(), für byte-first Code, der strenge Alphabete, strenge Pad-Bits und Streaming ohne Akrobatik will. Legen Sie den Zeichensatz fest, validieren Sie, was Ihnen Fremde schicken, vergleichen Sie Signaturen mit timingSafeEqual, und das Format hört auf, auf beiden Seiten der Browser/Server-Grenze ein Rätsel zu sein.

Und wenn Sie damit fertig sind, Pakete zu öffnen, denken Sie daran, dass sie jemand versiegeln musste. Die Kodierungs-Seite hat ihre eigenen Fallen: die Unicode-Mauer, die btoa() mitten im Satz stoppt, der MIME-Zeilenumbruch, die Padding-Regeln von base64url und das neue Uint8Array.toBase64() mit seiner omitPadding-Option. Diese Geschichte, mit Code-Beispielen für jeden Schritt, ist im Detail im verwandten Artikel zur Base64-Kodierung auf unserer Schwester-Site abgedeckt. Lesen Sie ihn als Nächstes, denn die Fallen sind auf der anderen Seite des Alphabets anders - und lustiger.

Zuletzt aktualisiert: 2026-09-07

Verwandter Artikel: Base64-Kodierung in JavaScript/Node.js: Ein vollständiger Leitfaden