Werk je met Base64-indeling? Dan is deze site perfect voor jou! Gebruik onze handige online tool om je gegevens te coderen of te decoderen.

Base64-decodering in JavaScript/Browser: een complete gids

Het duikt op in een dozijn verschillende vermommingen: een JWT verstopt in een Authorization-header, een image/png-blob in een JSON-response, een Sec-WebSocket-Accept-waarde in een handshake-log, een e-mailbijlage verpakt in MIME, een waarde die je backend beleefd in een query string stopte. De tekenreeks zelf ziet er altijd hetzelfde uit: een lange rij letters, cijfers, af en toe een + of /, en misschien nog een = of twee aan het eind. Als de startpagina van deze site je leerde wat Base64 is - vier afdrukbare tekens die voor elke drie bytes staan, met =-padding om de laatste groep af te maken - dan gaat dit artikel over het deel dat je daadwerkelijk in code doet: die tekens weer terugzetten naar bytes, en de bytes weer terug naar betekenis, met alleen wat de browser al meelevert.

Twee snelle basisregels voordat we beginnen. Eerst is decoderen de krimpende richting: voor elke vier tekens die je erin leest, komen er drie bytes uit, dus de uitvoer neemt altijd minder geheugen in dan de invoer. Ten tweede is een gedecodeerde Base64-tekenreeks niet automatisch tekst. Het zijn bytes, en bytes kunnen blijken UTF-8 te zijn, Windows-1252, een PNG-header of een cryptografische handtekening. De meest voorkomende bug in Base64-code is vergeten welke van die opties je in handen hebt, dus de secties hieronder zijn ingedeeld rondom die vraag.

De drie lagen van decoderen

Moderne browsers geven je drie native lagen, en het goede nieuws is dat er nooit een pakket nodig is. Elke laag beantwoordt een iets andere vraag, en de juiste kiezen bespaart je een hoop van die copy-paste Stack Overflow-fragmenten:

Lager Wat het opslorpt Wat het je aanreikt Personaliteit Beschikbaarheid
atob() standaard Base64-tekenreeks een "binaire tekenreeks" (één byte per teken) zeer vergevingsgezind: slaat ASCII-witte ruimtes over, accepteert ontbrekende padding elke browser sinds de 2000s, IE 10+, Node 16+
TextDecoder bytes (Uint8Array) leesbare JavaScript-tekst configureerbaar: label voor de charset, fatal-flag voor strengheid Firefox 18, Chrome 38, Safari 10.1 en nieuwer (nooit IE)
Uint8Array.fromBase64() Base64-tekenreeks plus opties een echte Uint8Array streng met knoppen: alfabet en afhandeling van de laatste chunk Baseline 2025: Chrome 140, Firefox 133, Safari 18.2, Node 25

De vorm van het hele artikel volgt uit die tabel. atob() is de werkpaard die je overal tegenkomt, ook in oude code. TextDecoder is de brug van bytes naar woorden. En Uint8Array.fromBase64() is de upgrade van 2025 die de tussenstap helemaal overslaat als je alleen maar bytes wilde hebben.

atob: snel, vergevingsgezind en zeer oud

De hele afspraak past in één regel: atob(encodedData). Het accepteert een Base64-gecodeerde tekenreeks en geeft een "binaire tekenreeks" terug: een gewone JavaScript-tekenreeks waarin elk teken exact één gedecodeerde byte bevat, een codepunt van 0 tot 255. Dat teruggeeftype is belangrijk, want het is niet hetzelfde als leesbare tekst (daarover meer hieronder). De functie zelf is zo snel als het maar kan, en het bestaat al heel lang: Chrome 4, Firefox 1, Safari 3, en - dat is die welke de meeste mensen onthouden - Internet Explorer pas vanaf versie 10, waardoor code die vóór 2012 is geschreven vol zit met handgemaakte Base64-tabellen.

Wat atob() aangenaam maakt, is hoeveel het vergeeft voordat het opgeeft. De WHATWG HTML-standaard zegt dat je alle ASCII-witte ruimtes moet negeren - spatie, tab, line feed, form feed, carriage return - voordat je decodeert, dus een in MIME verpakte tekenreeks met regeleinden om de 76 tekens decodeert zonder enige opruiming van jouw hand. Ontbrekende padding wordt ook vergeven. Maar zodra het een teken buiten het alfabet ziet, of een lengte die nooit geldig zou kunnen zijn, gooit het een DOMException met de naam InvalidCharacterError. Geen stille rommel, geen gedeeltelijke resultaten.

Hier is het schadeoverzicht, regel voor regel:

Invoer Resultaat
"SGVsbG8sIFdvcmxkIQ==" "Hello, World!" - het schoolboekgeval
"aGVsbG8" (geen padding) "hello" - een ontbrekende = wordt vergeven
"SGVs\nbG8s\nIFdvcmxkIQ==" (regels met regeleinden) "Hello, World!" - ASCII-witte ruimtes worden eerst overgeslagen
"" (lege tekenreeks) "" - de lege invoer is geldig en round-trippt
"A" (één overgebleven teken) gooit InvalidCharacterError - één teken kan niets encoderen
"Zm9vYmFy!" (losse !) gooit InvalidCharacterError - buiten het alfabet
"ZGFua29nYWk-" (URL-safe teken gemengd) gooit InvalidCharacterError - de twee alfabetten mogen niet gemengd worden
"Zm9v====" (te veel padding) gooit InvalidCharacterError - hoogstens twee = aan het eind

Één praktische opmerking: het foutbericht zelf verschilt tussen engines (Firefox zegt "String contains an invalid character", Chrome zegt dat de tekenreeks "characters outside of the Latin1 range" bevat bij niet-Latin1-invoer of "is not correctly encoded" bij ongeldige base64), dus vang op op de naam van de exceptie, niet op de tekst van het bericht.

Van ruwe bytes naar echte tekst

Die "binaire tekenreeks" als teruggeeftype verdient een puntje achter, want het is de bron van de meeste decoderingsverwarring. JavaScript-teksten zijn UTF-16, dus atob() reikt je een tekenreeks aan waarvan de tekens byte-waarden zijn, geen leesbare glyphs. Was je payload de UTF-8-codering van de tekst "hello 你好", dan levert direct afdrukken mojibake op. De oplossing is decoderen in twee stappen: Base64 naar bytes, dan bytes naar tekst.

Eerst de stap van Base64 naar bytes. Deze kleine helper is het klassieke recept en de moeite waard om in je zak te stoppen, want het is de dragende schakel in de meeste voorbeelden in dit artikel:

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;
}

Daarna de stap van bytes naar tekst, met TextDecoder. Voor UTF-8 (de standaard, en de juiste keuze voor JSON, JWT-payloads en de meeste webdata) is de aanroep één regel:

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

Waarom twee stappen überhaupt? Omdat atob() geen idee heeft in welke charset de bytes zijn geproduceerd. Het is een pure bit-omzetter. TextDecoder is het onderdeel dat bytes interpreteert als een charset, en het accepteert een label voor het werk: utf-8, windows-1252, iso-8859-1, utf-16le, plus zo'n 220 andere labels. Data die uit een applicatie van de jaren 90 komt, is meestal Windows-1252, en één constructor-argument is alles wat je nodig hebt:

const decoder = new TextDecoder('windows-1252');
const text = decoder.decode(bytes); // dezelfde bytes, andere interpretatie

De constructor van TextDecoder accepteert ook een fatal-flag, en het loont zich om die op true te zetten wanneer de gedecodeerde tekst ergens belangrijks in vloeit. Standaard is de decoder tolerant: ongeldige byte-sequenties worden stilletjes vervangen door het Unicode-vervangingsteken, U+FFFD, en je wordt het nooit verteld. Met fatal: true gooit dezelfde corruptie een TypeError in plaats van de fout te verbergen:

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

Dit is een van die schakelaars die in de docs onbeduidend lijken en in productie eruitzien als een data-incident. Als je invoer van gebruikers of van het netwerk komt, decodeer dan streng en behandel de fout met opzet.

URL-safe invoer heeft een omweg nodig

Één variant van Base64 verdient een eigen sectie, want die komt constant in het wild voor en atob() kan hem niet lezen. Het is het URL-en-bestandsnaamveilige alfabet uit sectie 5 van RFC 4648, gewoonlijk base64url genoemd: dezelfde 64 tekens, behalve dat + en / worden vervangen door - en _, en de =-padding wordt vaak weggelaten, omdat de datalengte impliciet bekend is. De ruil heeft een concrete reden: in een URL betekent + een spatie en / begint een padsegment, dus het standaardalfabet zou teken voor teken percent-encoded moeten worden. Base64url reist vlot door query strings, padsegmenten, fragments en bestandsnamen.

De val is dat de twee alfabetten niet uitwisselbaar zijn, en atob() spreekt alleen de standaard versie. Geef het een - of _ en je krijgt InvalidCharacterError. Je hebt twee schone opties.

Optie één, die overal werkt: converteer het alfabet en herstel de padding voordat je atob() aanroept:

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"

De expressie (4 - (s.length % 4)) % 4 is de hele truc: ze berekent hoeveel =-tekens een goed gepadde tekenreeks van die lengte nodig zou hebben, van nul tot hoogstens twee.

Optie twee, in browsers van 2025+: de nieuwe native decoder accepteert het alfabet als optie, dus helemaal geen tekenreeks-chirurgie nodig:

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

Twee regels houden je uit de problemen. Meng nooit alfabetten binnen één waarde - een decoder die zowel een + als een - ziet, heeft geen manier om te weten welke familie hij leest, en het conform-de-standaard gedrag is om te falen. En maak met de ander kant van de verbinding af of de padding er is of niet: weglaten is legaal voor base64url, dus een ontvanger moet gereed zijn voor beide vormen. atob() is dat al; de native opties hieronder geven je een knop voor dat gedrag.

De snelroute van 2025: Uint8Array.fromBase64

Kijk je terug naar de base64ToBytes-helper, dan merk je dat die twee dingen doet: Base64 decoderen, en daarna de tekens één voor één in JavaScript naar een byte-array kopiëren. Die kopieerloop is het trage, vermijdbare deel, en precies dat verwijdert de nieuwe ECMAScript-methode. Uint8Array.fromBase64(string, options) gaat rechtstreeks van de gecodeerde tekenreeks naar een byte-array, en het is uitgebracht in Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 en Deno 2.5 - het eerste JavaScript-platformfeature van dit soort dat landde, gemarkeerd als Baseline Newly available in het Baseline-programma van de browservendors.

Het object met opties heeft twee knoppen. De eerste is alphabet: "base64" (de standaard) of "base64url". De tweede is lastChunkHandling, dat bepaalt wat er gebeurt met de laatste onvolledige groep tekens:

Modus Regel voor de laatste chunk
"loose" (standaard) twee of drie tekens, of vier met padding; overgebleven overloop-bits worden genegeerd
"strict" exact vier tekens (padding alleen waar de lengte dat vereist), en de overloop-bits moeten allemaal nul zijn
"stop-before-partial" alleen complete groepen van vier tekens worden gedecodeerd; een onvolledige staart blijft ongelezen

Net als atob() negeert de methode ASCII-witte ruimtes in de invoer, dus regels met regeleinden zijn geen probleem. In tegenstelling tot atob() heeft het wel een mening over alles anders: een teken buiten het gekozen alfabet, of een laatste chunk die de gekozen modus schendt, gooit een SyntaxError; iets anders dan een tekenreeks doorgeven gooit een TypeError. Hier is strict-modus in actie, die een chunk waarvan de padding ontbreekt afwijst:

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

Prestatie is de andere reden om het te verkiezen. Op een recente Firefox op de machine van de auteur neemt het decoderen van een payload van 10 megabyte slechts enkele millisecondes in beslag met fromBase64, terwijl de klassieke atob plus teken-voor-teken byte-omzetting zo'n twintig keer zo lang duurt, omdat het trage deel de loop op JavaScript-niveau is, niet het Base64-rekenwerk. Als je data bytes zijn, sla de tekenreeks dan helemaal over.

Voor oudere browsers is de situatie simpel: houd de base64ToBytes-helper hierboven aan, of trek een klein polyfill aan (core-js en het es-arraybuffer-base64-pakket van het es-shims-project leveren allebei er een voor fromBase64) als je overal nieuwe-stijl code wilt schrijven. De API is stabiel - het zit nu in de ECMAScript-specificatie - dus wat je er tegenaan schrijft, zal niet worden afgeschaft.

Een JWT lezen

De meest voorkomende "mysterieuze tekenreeks" in applicatielogs is een JSON Web Token: drie door punten gescheiden segmenten, header.payload.signature, waarvan de eerste twee base64url-gecodeerde JSON-objecten zijn. Eentje decoderen is een kwestie van vijf regels, en het is de perfecte opwarmer voor alles wat hierboven aan bod kwam:

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"

En nu het deel dat beginners overslaan en productiesystemen op de harde manier leren: de payload is niet geverifieerd doordat hij decodeerbaar is. Iedereen kan een JWT schrijven met welke payload hij wil; het signatuursegment is wat hem aan een geheim koppelt. Een HS256-token verifiëren in de browser gebruikt de Web Crypto API, die de signatuur als bytes nodig heeft - nog een reden waarom de segment-naar-bytes-helper zijn kost verdient:

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 alleen als de signatuur met het geheim overeenkomt

Drie valkuilen verdienen een naam. Eerst, controleer de header voordat je verifieert: een token dat alg: "none" beweert, vraagt je de payload te vertrouwen zonder signatuur, en naïeve code is al misleid om precies dat te doen. Tweede, respecteer de tijdclaims - exp, nbf, iat - na verificatie, niet ervoor. Derde, de klassieke key-confusion-aanval: een server die voor RS256 is geconfigureerd maar die ook HS256 accepteert, laat een aanvaller tokens tekenen met de openbare sleutel (die opzettelijk publiek is) als HMAC-geheim. In het kort: decodeer vrij, vertrouw niets, verifieer alles.

Data URLs openen

Een data URL embedt een heel bestand in een URL: data:, een optionele media type, een optionele ;base64-flag, een komma, en dan de payload. Tekst-payloads zijn percent-encoded, binaire payloads zijn Base64, en de browser rendert ze zonder enig HTTP-verzoek - geen fetch, geen serverreis, niets om te cachen. De browser behandelt elke data URL als een unieke, ondoorzichtige origin, en dat is ook de reden waarom ze een favoriet kanaal zijn voor slimmige content: een data:text/html-document dat in een iframe wordt geopend, voert zijn scripts uit, en een restrictieve Content-Security-Policy kan data URLs helemaal blokkeren. Houd je CSP in gedachten als je begint om deze door te geven aan door de gebruiker besturde markup.

Eentje decoderen is grotendeels tekenreeks-chirurgie, en daarna hetzelfde bytes-pijpwerk als hierboven:

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);

Het meta-deel vertelt je de media type (hier image/png, met de ;base64-marker die bevestigt dat de payload Base64 is). Is de payload eenmaal een Blob, dan geldt alles normale: een object URL voor een <img>, een downloadlink, of een POST-verzoek naar een server. De enige echte kosten van de data-URL-route is de grootte - de payload zit zo'n 33 procent groter dan het originele bestand - en een groot beeld in een URL kan de tekenreeksbeperkingen van de pagina belasten, wat nog een stem is voor object URLs wanneer het bestand de browser nooit hoeft te verlaten.

Bestanden decoderen die als tekst aankomen

Bestanden bereiken de browser op twee manieren. De moderne manier is ruwe bytes: een fetch die je leest als een ArrayBuffer, of een File uit een picker die je leest met file.arrayBuffer(). Zit je op dat pad, gefeliciteerd - er is helemaal geen Base64 bij betrokken, en je moet op dat pad blijven, want bytes kosten niets om mee te dragen, terwijl Base64 een derde extra aan bandbreedte en geheugen kost voor het voorrecht. De andere manier is wanneer het kanaal alleen tekst kan: een JSON-API die {"attachment": "data:application/pdf;base64,JVBERi..."} teruggeeft, een e-mailbijlage, een configuratietekenreeks, een waarde in een databasekolom. Dan is Base64 het protocol, en je werk is om de bytes eruit te halen:

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]);

Drie opmerkingen over dat fragment. Splitsen op de eerste komma is alles wat je nodig hebt om de data-URL-header af te schillen (de media type kan geen komma bevatten, dus de eerste is altijd de scheiding). En als de waarde gewoon Base64 is zonder data-URL-prefix, sla de split dan gewoon over. Ten slotte is die naakte await een top-level await, en browsers staan die alleen toe binnen modules, dus het fragment heeft een <script type="module">-tag of een async-wrapper rond die twee regels nodig. MIME-onderdelen in e-mail zijn hetzelfde verhaal met extra stappen: het lichaam van de bijlage is Base64 in regels van 76 tekens, maar omdat atob() witte ruimtes overslaat, kun je hem de tekst met regeleinden gewoon geven zoals die in het rauwe bericht aankwam - het hoeft niet eerst weer tot één lange regel samengevoegd te worden. Die ene eigenschap bespaart stilletjes een hoop regex.

Een WebSocket-handshake verifiëren

Één van de charmantste toepassingen van decoderen in de browser is de WebSocket-handshake zelf controleren. RFC 6455 vereist dat de client een Sec-WebSocket-Key-header stuurt (16 willekeurige bytes, Base64-gecodeerd) en dat de server antwoordt met Sec-WebSocket-Accept: de SHA-1-hash van de key geconcateneerd met een vaste magische GUID, Base64-gecodeerd. Klopt de waarde niet, dan faalt de handshake en wordt de verbinding niet ge-upgraded. Het hele punt van dit ceremonieel is dat een server die alleen HTTP spreekt, het niet per ongeluk kan voltooien - de magische GUID bestaat om de berekening opzettelijk overdreven ingewikkeld te laten lijken. En omdat de browser zowel het hashen als het coderen heeft, kun je het verwachte antwoord zelf berekenen, waardoor het debuggen van proxys en gateways een regel wordt:

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 laatste regel is geen toeval - het is het exacte voorbeeld uit de RFC, byte voor byte nagemaakt. Antwoordt jouw gateway met iets anders, dan weet je nu precies welke kant van de vergelijking liegt.

HTTP-headers en query strings

Base64 is populair voor HTTP-headers, omdat headers ASCII moeten zijn, en het beroemdste geval is Basic-authenticatie: Authorization: Basic gevolgd door de Base64-codering van username:password. Zo'n header lezen (stel, terwijl je toont wat een verzoek meedraagt) is één split en één decode:

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

Het spreid-en-weer-samen-voegenpatroon lost het onhandige maar legale geval van een wachtwoord met een dubbele punt op, want het splittingspunt is altijd het eerste na de gebruikersnaam. Hetzelfde patroon geldt overal waar een header een gestructureerde waarde smokkelt: Proxy-Authorization, sommige vendor-specifieke headers, en de af en toe voorkomende cookie. In query strings en deeplinks verschijnt Base64 wanneer een app state wil delen zonder server: een OAuth state-waarde, een hersteld zoekformulier, een "verder waar ik gebleven was"-marker. Decodeer defensief - pak in try/catch, want de waarde is een netwerkgrens overgestoken en er kan alles mee zijn gebeurd - en behandel wat je krijgt als niet te vertrouwen invoer, punt.

En dat brengt ons bij de zin die boven elke terminal zou moeten hangen: Base64 is geen encryptie. Het is niet eens obfuscatie in echte zin, want de "decryptie" is één functieaanroep die elke taal ter wereld implementeert. Moet een waarde geheim blijven, dan maakt het eerst Base64-coderen het minder veilig, niet meer - het creëert de illusie van privacy en voegt precies één triviale stap toe voor wie het origineel wil.

State in de URL en in opslag

Dezelfde logica strekt zich uit tot alles dat een paginavernieuwing of een deellink moet overleven. De gebruikelijke kandidaten: localStorage en sessionStorage-waarden die gestructureerde of binaire data dragen, het hash-fragment van een URL voor de route-status van een single-page-app, en configuratieblokken die door bouwtijdtools in pagina's worden geplaatst. Het opslagverhaal verdient één concreet voorbeeld, want de leeszijde is het paar van de schrijzijde die je je zult willen herinneren:

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

Drie dingen om in gedachten te houden. Eerst, budgetten: browsers geven elke origin zo'n 5 megabyte localStorage, en je opgeslagen Base64-tekenreeks eet zo'n 33 procent meer dan de originele data, dus een bestand van 3,5 megabyte wordt stilletjes 4,6 megabyte opslag - en de tekenreeks leeft in geheugen als UTF-16, wat de voetafdruk zolang de pagina open is nog eens verdubbelt. Tweede, consistentie: codeer en decodeer met dezelfde charset aan beide kanten, anders sla je prima bytes op en lees je mojibake. Derde, deellinks: reist de state via de URL, gebruik dan het URL-veilige alfabet zodat de waarde copy-paste overleeft, en houd het kort, want URL-lengtes van boven de twee duizend tekens beginnen oude clients en logboeken onzeker te maken.

Wanneer de data in stukjes aankomt

Soms komt de Base64 niet als één tekenreeks binnen: een WebSocket-berichtsgrens snijdt hem doormidden, een server-sent event-stream druppelt hem binnen, een chunked upload voedt hem enkele kilobytes tegelijk. Je kunt atob() niet op een fragment aanroepen, want Base64-groepen zijn 3-byte-eenheden uitgedrukt in blokken van 4 tekens, en een snede in het midden van een groep laat een hangend fragment achter. De klassieke oplossing was tekens bufferen tot je een veelvoud van vier had en de buffer in stukken decoderen. De 2025-API maakt dit schoon: Uint8Array.prototype.setFromBase64(string, options) schrijft gedecodeerde bytes in een bestaand array en geeft een object terug met twee getallen, read (hoeveel tekens het verbruikte) en written (hoeveel bytes het produceerde). Met lastChunkHandling: "stop-before-partial" decodeert het alleen complete groepen en laat de onvolledige staart ongelezen, wat precies het gedrag is dat een streamdecoder wil:

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);

Lees de binnenste loop langzaam, want dat is het hele patroon: geef de overgedragen rest plus het nieuwe stuk in, laat de decoder zo veel complete groepen verbruiken als er passen, onthoud hoeveel er over was door result.read tekens af te snijden, en als er niets compleets meer over is (result.read === 0) berg dan de rest op als de nieuwe carry en wacht op het volgende stuk. De Uint8Array(8) is alleen een tussentijdse buffer - één groep van vier tekens produceert hoogstens drie bytes, dus acht is ruim. Aan het eind bevat carry wat de stream nooit afmaakte, en dat is óf je foutsignaal óf je "verbinding eindigde schoon"-controle.

Wanneer je Base64 niet moet decoderen

Een bruikbare referentie leert je wanneer je het gereedschap neerlegt. Controleer je beide uiteinden van het kanaal, pak dan ruwe bytes: fetch met response.arrayBuffer() voor downloads, file.arrayBuffer() voor picker-bestanden, ArrayBuffer-payloads in WebSockets, en multipart FormData voor uploads. Geen enkel daarvan raakt Base64 aan, en je krijgt de data op volle snelheid, zonder de groottenbelasting en zonder de voetafdruk van een tekenreeks in geheugen. Base64 verdient zijn kost precies wanneer het kanaal alleen tekst kan: JSON-lichamen, query strings, e-mail, opslag, legacy-API's, en alles waarvan het contract zegt "ASCII of niks". Het moment dat een byte zou volstaan, betaalt een Base64-tekenreeks een toeslag van 33 procent voor het voorrecht afdrukbaar te zijn, en de toeslag wordt in rekening gebracht in bandbreedte, geheugen en CPU - drie rekeningen die je allemaal kunt vermijden.

Gemeenschappelijke decoderingsvalkuilen

Na al de succespaden, hier is de lijst met manieren waarop dit bijt, in ongeveer de volgorde waarin je ze zult tegenkomen:

  • Het resultaat van atob() als tekst behandelen. Het is een binaire tekenreeks. Via TextDecoder wordt het tekst; direct afgedrukt, wordt het mojibake. Deze ene verwarring veroorzaakt de meeste "Base64 werkt niet"-meldingen.
  • Verwachten dat Unicode zomaar werkt. De bytes van "你好" decoderen prima, maar ze zijn gewoon bytes totdat een decoder zegt dat het UTF-8 is. Codeer en decodeer aan beide kanten in dezelfde charset.
  • Base64url aan atob() voeren. Een enkele - of _ gooit een exceptie. Converteer eerst het alfabet, of gebruik fromBase64 met de juiste optie.
  • Geloven dat elke lange tekenreeks Base64 is. Een geldige, gepadde Base64-tekenreeks heeft een lengte die een veelvoud van vier is (base64url zonder padding mag eindigen op 2 of 3) en gebruikt hoogstens één alfabet. Een lengte van één modulo vier faalt direct - controleer dat voordat je er een try/catch voor gebruikt.
  • Padding vertrouwen dat je niet afgesproken hebt. Sommige systemen halen de = weg, sommige houden hem aan, en sommige voegen hem toe in het midden van een tekenreeks met regeleinden, waar hij niet thuishoort. Maak met de afzender af, en beslis dan of je soepel (atob) of streng (fromBase64) wilt zijn.
  • Stille corruptie door een leniente decoder. Een standaard TextDecoder vervangt ongeldige bytes door U+FFFD en zegt niets. Zet fatal: true wanneer de data ertoe doet.
  • Aannemen dat Base64 iets beschermt. Dat doet het niet. Het is een serialisatieformaat, één functieaanroep van platte tekst, en "we Base64 het zodat gebruikers het niet kunnen lezen" is een veiligheidspostuur, geen controle.
  • Geheugen vergeten. Een gedecodeerde binaire tekenreeks van één megabyte bezet twee megabyte als UTF-16-tekenreeks, terwijl een Uint8Array van dezelfde data één megabyte bezet. Voor grote payloads ga je rechtstreeks naar fromBase64.
  • Opnieuw decoderen bij elke render. Een paar megabyte decoderen is snel, maar niet gratis - en het is niet iets om per frame te doen. Decodeer één keer, sla de bytes in de cache op, render uit de cache.

Prestatienotities

De korte versie: de native decoders zijn snel, en het trage deel van oude code is meestal de JavaScript eromheen, niet de Base64 zelf. Op de groottes die ertoe doen, is het beeld hetzelfde: een payload van 10 megabyte decodeert in enkele millisecondes met Uint8Array.fromBase64; atob alleen is een paar keer trager, en de klassieke volgende loop die tekens naar een byte-array zet, duurt voor dezelfde invoer zo'n twintig keer zo lang als fromBase64, omdat hij zo'n dertien miljoen eigenschapswrites op de hoofdthread uitvoert. Praktische gevolgen: gebruik fromBase64 bij voorkeur waar je publiek het heeft; houd de atob-helper aan waar ze het niet hebben; bouw nooit een byte-array door in een loop tekenreeksen te concatenëren; en als je een enorme payload moet verwerken, overweeg dan de gedecodeerde Uint8Array aan een Web Worker te geven - de bytes worden overgezet zonder te kopiëren, en de hoofdthread blijft vrij om de UI op 60 frames per seconde te houden. En onthoud de richting van de rekenkunde: decoderen krimpt, dus een gedecodeerde buffer neemt altijd minder geheugen in dan de tekenreeks waar hij vandaan kwam. Je kunt nooit door decoderen tegen het einde van je geheugen lopen; je kunt alleen tegen het einde van je geheugen lopen door zowel de tekenreeks als de bytes langer in handen te houden dan nodig.

Een korte geschiedenis van decoderen in browsers

Base64 is ouder dan het grootste deel van het moderne web, maar de decoders van de browser hebben een verhaal dat de moeite van het kennen waard is, want het legt uit waarom het ecosysteem vol relicten zit. atob en zijn zuster btoa zijn ouder dan de specificatie die ze nu dekt: de WHATWG HTML-standaard definieerde ze pas in februari 2011, toen hun lang bestaand browsergedrag achteraf in de standaard werd vertaald. De engines hadden ze al vroegtijdig uitgebracht: Firefox vanaf versie 1 in 2004, Safari 3, Chrome 4. Internet Explorer sloeg ze helemaal over tot IE 10 in 2012, waardoor JavaScript vóór 2012 een museum is van handgemaakte Base64 - opslagtabels, String.fromCharCode-acrobatiek, en de beruchte unescape(encodeURIComponent())-incantatie voor Unicode, een paar functies dat in de taal was afgeschaft en tien jaar in browsers overleefde door pure traagheid. Daarna kwam de charset-laag: TextEncoder en TextDecoder uit de Encoding-standaard arriveerden tussen 2013 en 2017 (Firefox 18, Chrome 38, Safari 10.1, en nooit in enige IE), waardoor het platform eindelijk een principiele manier had om bytes naar woorden om te zetten. Node.js, dat atob en btoa als globalen pas vanaf versie 16 in 2021 had, bracht zijn vroege leven door met Buffer en een paar kleine npm-shims. En toen sloot de cirkel: Firefox 133 (november 2024) en Safari 18.2 (december 2024) brachten Uint8Array.fromBase64, toBase64 en de anderen eerst uit, en de tweede helft van 2025 voltooide de set toen Chrome 140 (september) en Node 25 (medio oktober) landden en het Baseline-programma ze Newly available markeerde, de eerste keer dat de taal zelf - niet het webplatform - Base64 ingebouwd kreeg. Een decennia oud formaat werd zo een standaardbibliotheekfeature van de taal, en de code van het volgende decennium hoeft niet meer met helpers rond te kopiëren.

Leuke feiten

  • De snelste "is dit überhaupt Base64?"-test die bestaat, is string.length % 4 === 0. Elke geldige, gepadde Base64-tekenreeks slaagt; alles andere is een vreemdeling.
  • atob('') geeft '' terug. De lege tekenreeks is de enige invoer zonder bytes, en hij round-trippt schoon door de hele pipeline - geen speciaal geval nodig, nooit.
  • De magische GUID van WebSocket, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, is een vaste waarde, ingebakken in de RFC, gekozen zodat een gewone HTTP-server de handshake nooit per ongeluk kon voltooien. Het is de beroemdste constante in de protocolengineering die niemand ooit genereert.
  • Chrome en Firefox gooien bij dezelfde mislukking dezelfde exceptie, maar met verschillende berichten. Vang op op error.name, niet op de berichttekenreeks, anders krijgt je foutafhandeling een browseraccent.
  • Een binaire tekenreeks van één megabyte weegt twee megabyte in geheugen, omdat JavaScript-teksten UTF-16 zijn: elke gedecodeerde byte reist mee met een byte onbenutte marge. De Uint8Array heeft zo'n belasting niet.
  • "Data URI" is een gepensioneerde naam. De WHATWG hernoemde het naar "data URL" als onderdeel van de grote URI-naar-URL-harmonisatie, waardoor je beide schrijfwijzen zult tegenkomen in specificaties, posts en pakketnamen.
  • RFC 4648 levert een tabel met testvectoren - "f", "fo", "foo", "foob", "fooba", "foobar" en de anderen, elk met zijn bekende codering - waar decoderingsauteurs al twintig jaar tegen checken. Als je decoder die rijen doorstaat, is hij vrijwel zeker correct.
  • De meest geproduceerde Base64-tekenreeks in de geschiedenis van de informatica is vrijwel zeker aGVsbG8=, de codering van "hello". Elke "aan de slag"-tutorial, test suite en Stack Overflow-antwoord op de planeet draagt zijn stem bij.

Samenvatting

Dus past het hele ambacht van decoderen in de browser op één pagina: atob() voor de snelle, vergevingsgezinde, universele decode; TextDecoder om de bytes om te zetten in de woorden die je echt wilt, met fatal: true wanneer de data ertoe doet; en Uint8Array.fromBase64 voor de moderne, strenge, snelle route die de tekenreeks helemaal overslaat. Daar tussenin hebben de varianten namen en regels: base64url voor alles wat via een URL reist, padding die er wel of niet is, witte ruimtes die de oude decoder stilletjes opslorpt. En onder alles twee houdingen: de bytes zijn niet de tekst, en de tekst is niet het geheim. Decodeer met opzet, verifieer voordat je vertrouwt, en wanneer het kanaal het toelaat, sla Base64 over en neem de bytes.

De andere helft van de reis - je bytes en tekst nemen en er de afdrukbare tekenreeks van maken waarmee al dit begon - wordt in detail behandeld in de daarbij behorende gids over Base64-coderen in JavaScript, hieronder gelinkt.

Laatst bijgewerkt: 2026-10-06

Gerelateerd artikel: Base64-codering in JavaScript/Browser: een complete gids