[JS] 10 Min. Lesezeit

[JS] Base64 in JavaScript — atob, btoa und Node Buffer

Ein praktischer Leitfaden zu Base64 in JavaScript. Wann Sie btoa/atob nutzen, warum sie bei Unicode scheitern, wie Buffer in Node.js arbeitet, plus ein laufzeitübergreifender base64url-Helfer.

April 2026 | javascript

// DIE AUSGANGSLAGE

Die Geschichte von Base64 in JavaScript ist historisch fragmentiert. Der Browser hat btoa() / atob() in den 1990er-Jahren von Netscape geerbt — Funktionen, deren Namen wörtlich für binary-to-ASCII und ASCII-to-binary stehen. Sie verarbeiten nur Latin-1-Zeichen; genau deshalb wirft btoa() einen Fehler, wenn Sie Emojis oder chinesischen Text hineinkopieren.

Node.js hat seinen eigenen Weg über die Klasse Buffer eingeschlagen. Sie kennt jede Kodierung, die Sie brauchen dürften (utf8, base64, base64url, hex, latin1, ascii), geht nativ mit Binärdaten um und ist die naheliegende Wahl für Serverseiten-Code.

Moderne Laufzeiten (Node 18+, Deno, Bun, moderne Browser) liefern zudem TextEncoder / TextDecoder — den standardbasierten Weg, Unicode-Strings vor dem Base64-Kodieren in Bytes zu verwandeln. Zu wissen, zu welchem Werkzeug man greift, ist die halbe Miete.

// BROWSER: btoa / atob — DIE KORREKTE VERWENDUNG

btoa(str) nimmt einen String entgegen, in dem jedes Zeichen in 1 Byte passen muss (Latin-1-Bereich, U+0000 bis U+00FF), und liefert einen Base64-String zurück. atob(b64) kehrt dies um. Beide sind synchron, schnell und seit Urzeiten in jedem Browser eingebaut.

Der richtige Einsatz lautet: bereits byte-artige Daten Base64-kodieren. Rohbytes aus einem FileReader, aus einem binären XMLHttpRequest (responseType: 'arraybuffer') oder aus einem Uint8Array, wobei der numerische Wert jedes Bytes in 0–255 liegt.

// Encode a Latin-1 string
const latin1 = 'Hello, world!';
const encoded = btoa(latin1);   // 'SGVsbG8sIHdvcmxkIQ=='

// Decode it back
const decoded = atob(encoded);  // 'Hello, world!'

// Encode a binary Uint8Array (the common real-world case)
const bytes = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0]);  // JPEG magic
const binStr = String.fromCharCode(...bytes);            // 'ÿØÿà' (Latin-1)
const b64 = btoa(binStr);                                // '/9j/4A=='

// WARUM btoa() BEI UNICODE SCHEITERT

Geben Sie in einer Browser-Konsole btoa('héllo') ein. Sie erhalten InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range. Das é (U+00E9) geht noch, aber btoa('日本語') scheitert, weil (U+65E5) außerhalb von Latin-1 liegt. Die Funktion kennt kein UTF-8; sie liest jede JavaScript-String-Code-Unit als einzelnes Byte.

Die Lösung besteht darin, den Unicode-String zuerst in UTF-8-Bytes zu verwandeln und erst diese Bytes Base64 zu kodieren. Jeder moderne Browser liefert TextEncoder genau dafür mit.

// ❌ broken
// btoa('日本語')  → throws InvalidCharacterError

// ✅ correct: encode to UTF-8 first
function b64encodeUtf8(str) {
  const bytes = new TextEncoder().encode(str);
  let bin = '';
  for (const byte of bytes) bin += String.fromCharCode(byte);
  return btoa(bin);
}

b64encodeUtf8('日本語');   // '5pel5pys6Kqe'

// ✅ round-trip decode
function b64decodeUtf8(b64) {
  const bin = atob(b64);
  const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

b64decodeUtf8('5pel5pys6Kqe');  // '日本語'

// NODE.JS: DER Buffer-ANSATZ

Greifen Sie in Node zu Buffer. Er versteht UTF-8 standardmäßig, unterstützt alle denkbaren Kodierungen und behandelt Binärdaten sauber. Node 16+ kennt zudem 'base64url' als Kodierungsname, sodass Sie für URL-sichere Ausgaben keinen separaten Helfer benötigen.

// String → Base64
const encoded = Buffer.from('héllo 世界').toString('base64');
// 'aMOpbGxvIOS4lueVjA=='

// Base64 → string (UTF-8 by default)
const decoded = Buffer.from(encoded, 'base64').toString();
// 'héllo 世界'

// URL-safe (Node 16+): +/- omitted padding
const safe = Buffer.from('héllo 世界').toString('base64url');
// 'aMOpbGxvIOS4lueVjA'  ← no = padding, uses -_

// Binary file → Base64
const fs = require('node:fs');
const imgB64 = fs.readFileSync('logo.png').toString('base64');

// Base64 → binary file
fs.writeFileSync('out.png', Buffer.from(imgB64, 'base64'));

// BUFFER VS. btoa/atob — KURZVERGLEICH

  • > Unicode: Buffer beherrscht UTF-8 nativ; btoa/atob verarbeiten nur Latin-1-Bytes.
  • > Binär: Buffer ist der native Binärtyp in Node; Browser müssen Uint8Array + String.fromCharCode nutzen.
  • > URL-sicher: Buffer unterstützt 'base64url' als erstklassige Kodierung (Node 16+); Browser benötigen eine eigene Regex-Ersetzung.
  • > Performance: Buffer ist in C++ implementiert und bei großen Eingaben ca. 3–5-mal schneller als atob/btoa.
  • > Streaming: Buffer lässt sich durch Transform-Streams für große Dateien pipen; btoa/atob können nicht streamen.
  • > Verfügbarkeit: btoa/atob existieren in Node 16+ als Globale, doch greifen Sie lieber zu Buffer, wenn Sie die Wahl haben — er bewältigt die Sonderfälle.

// BINÄRDATEIEN IM BROWSER KODIEREN UND DEKODIEREN

Das klassische Browsermuster, um eine Datei-Auswahl in einen Base64-String zu verwandeln, ist FileReader.readAsDataURL(), das eine sofort nutzbare Data-URI zurückgibt. Wenn Sie nur die rohen Base64-Daten möchten, entfernen Sie das Präfix data:...;base64,.

Lesen Sie bei sehr großen Dateien mit readAsArrayBuffer() und kodieren Sie in Häppchen, um den Fehler 'maximum call stack size exceeded' zu vermeiden, den String.fromCharCode(...bytes) bei Arrays mit mehr als ca. 65.000 Elementen auslöst.

// ▸ Simple: file → data URI → strip prefix
async function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const dataUri = reader.result;                      // 'data:image/png;base64,iVBOR...'
      resolve(dataUri.split(',')[1]);                     // raw Base64 only
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

// ▸ Large files: chunked btoa() to avoid stack overflow
async function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  const chunkSize = 0x8000;      // 32 KB chunks
  let binary = '';
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null,
      bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

// Usage
const file = inputEl.files[0];
const buf = await file.arrayBuffer();
const b64 = await arrayBufferToBase64(buf);

// BASE64 IM BROWSER ZU BYTES DEKODIEREN

// ▸ Base64 → Uint8Array (binary bytes)
function base64ToBytes(b64) {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes;
}

// ▸ Base64 → Blob → downloadable
function base64ToBlob(b64, mime = 'application/octet-stream') {
  const bytes = base64ToBytes(b64);
  return new Blob([bytes], { type: mime });
}

// Trigger a download
const blob = base64ToBlob(b64, 'image/png');
const url = URL.createObjectURL(blob);
Object.assign(document.createElement('a'), { href: url, download: 'out.png' }).click();
URL.revokeObjectURL(url);

// URL-SICHERES BASE64 (base64url) IN JAVASCRIPT

Standard-Base64 verwendet + und /, die beide in URLs reservierte Zeichen sind. Außerdem endet es mit einem oder zwei =-Zeichen, die zu %3D prozentkodiert werden und Platz verschwenden. RFC 4648 §5 definiert die URL-sichere Variante (base64url): tauschen Sie + gegen -, / gegen _ und verzichten Sie auf das Padding.

In Node 16+ nutzen Sie einfach Buffer.from(x).toString('base64url'). Im Browser implementieren Sie es als Nachverarbeitungsschritt auf btoa().

// ▸ Browser: standard Base64 → URL-safe Base64
function toBase64Url(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// ▸ URL-safe Base64 → standard Base64 (before feeding atob)
function fromBase64Url(b64u) {
  let b64 = b64u.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';   // re-add padding
  return b64;
}

// Round-trip
const token = toBase64Url(btoa('hello+world/'));
// 'aGVsbG8rd29ybGQv'
atob(fromBase64Url(token));  // 'hello+world/'

// HÄUFIGE FALLEN UND WIE SIE DIESE ERKENNEN

  • > InvalidCharacterError aus btoa() — Ihre Eingabe enthält Zeichen außerhalb von Latin-1. Fix: vorher mit TextEncoder nach UTF-8 kodieren.
  • > InvalidCharacterError aus atob() — Ihre Eingabe ist kein gültiges Base64. Meist fehlt das Padding (bei base64url) oder es sind Leerzeichen enthalten. Entfernen Sie Whitespace und fügen Sie =-Padding wieder hinzu.
  • > Kaputtes Unicode beim Dekodieren — Sie haben mit atob() direkt dekodiert, statt den Umweg über TextDecoder zu gehen. Mehrbyte-UTF-8-Sequenzen erscheinen als Mojibake.
  • > 'Maximum call stack size exceeded' — Sie haben String.fromCharCode(...bytes) auf ein Array mit mehr als ca. 65.000 Bytes angewandt. Fix: in Häppchen zerlegen (siehe die 32-KB-Schleife oben).
  • > Unterschiedliche Ausgaben zwischen Browser und Node — bedeutet meist, dass eine Seite URL-sicher und die andere Standard ist. An der Schnittstelle auf eine Variante normieren.
  • > Doppelt kodierte Nutzdaten — die Daten wurden zweimal Base64-kodiert. Prüfen Sie: sieht atob(input) erneut wie Base64 aus? Dann einmal zusätzlich dekodieren.
  • > Base64 in JSON versehentlich JSON-kodiert — Sie haben einen Base64-String in JSON.stringify gepackt, der Empfänger parst JSON und bekommt den String unverändert, doch Ihr Servercode hat ihn versehentlich erneut kodiert. Inspizieren Sie die tatsächlichen Daten auf der Leitung.

// LAUFZEITÜBERGREIFENDER HELFER (Browser + Node + Deno + Bun)

// A portable Base64/base64url helper. Uses Buffer when available,
// falls back to btoa/atob + TextEncoder in the browser / Deno.

const hasBuffer = typeof Buffer !== 'undefined';

export function encode(str, { urlSafe = false } = {}) {
  if (hasBuffer) {
    return Buffer.from(str, 'utf8').toString(urlSafe ? 'base64url' : 'base64');
  }
  const bytes = new TextEncoder().encode(str);
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  const b64 = btoa(bin);
  return urlSafe
    ? b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
    : b64;
}

export function decode(b64, { urlSafe = false } = {}) {
  if (hasBuffer) {
    return Buffer.from(b64, urlSafe ? 'base64url' : 'base64').toString('utf8');
  }
  if (urlSafe) {
    b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
    while (b64.length % 4) b64 += '=';
  }
  const bin = atob(b64);
  const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

// Usage
encode('héllo 世界');                      // 'aMOpbGxvIOS4lueVjA=='
encode('héllo 世界', { urlSafe: true });    // 'aMOpbGxvIOS4lueVjA'
decode('aMOpbGxvIOS4lueVjA==');            // 'héllo 世界'

// HINWEISE ZUR PERFORMANCE

  • > Buffer ist am schnellsten — in Node läuft er in C++ und verarbeitet Eingaben im Megabyte-Bereich ohne Stottern.
  • > btoa/atob sind konkurrenzfähig bei kleinen Nutzdaten (< 100 KB) im Browser; beide sind synchron und blockieren bei großen Eingaben den Haupt-Thread.
  • > Bei Eingaben > 10 MB im Browser verwenden Sie einen Worker, damit die Kodierung neben dem Haupt-Thread läuft — oder nutzen Sie Blob.text() + FileReader.readAsDataURL(), die intern optimiert sind.
  • > Vermeiden Sie wiederholte Encode/Decode-Zyklen — jeder Round-Trip ist O(n) in Zeit und Speicher. Cachen Sie die Base64-Form, wenn Sie sie mehrfach benötigen.
  • > Streaming — bei wirklich großen Nutzdaten (100 MB+) nutzen Sie Nodes stream.Transform oder die TransformStream-API des Browsers mit einem chunk-orientierten Base64-Encoder.

// VERWANDTE LEKTÜRE

Was ist Base64 und wie funktioniert es? — der 6-Bit-Gruppierungs-Trick von Grund auf erklärt.
Base64 für UTF-8 und Unicode — der TextEncoder-Tanz in aller Tiefe.
URL-sicher vs. Standard-Base64 — wann Sie -_ statt +/ benötigen.
Live-Base64-Encoder ausprobieren — Text einfügen, Kodierung sehen, Ergebnis kopieren.

Setzen Sie ein Lesezeichen auf diese Seite, falls Sie je wieder zu btoa() greifen — in neun von zehn Fällen steht die Frage, die Sie gleich googeln werden, genau hier.