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-codering in JavaScript/Browser: een complete gids

Je hebt iets dat moet reizen, en de weg is alleen breed genoeg voor platte ASCII. Het kan een afbeelding zijn die in een JSON-response thuishoort, een configuratieobject dat mee moet in een URL, een token waarvan de drie segmenten uit punten en letters bestaan, of een bestand waarvoor een API eist dat het als Base64-tekenreeks in een JSON-lichaam aankomt. Base64 is de tolpoost voor precies deze situatie, en de startpagina van deze site loopt het formaat al door - vier afdrukbare tekens die voor elke drie bytes staan, met =-padding om de groep af te maken - dus hier is het ene getal dat je in je hoofd moet houden tijdens het lezen: coderen is de groeiende richting. Elke drie bytes die je overhandigt, komen terug als vier tekens, een groottenbelasting van zo'n 33 procent, in rekening gebracht in bandbreedte, opslag en geheugen. Gebruik Base64 wanneer het kanaal afdrukbare tekst eist, en weet exact wat die belasting je kost.

Het bemoedigende nieuws is dat de browser dit werk al langer kan zonder een enkel pakket. btoa() wordt al sinds begin jaren 2000 meegeleverd, TextEncoder maakte een decennium geleden van je echte Unicode-tekst eerlijke bytes, en in de Baseline 2025-golf voegde het platform eindelijk Uint8Array.toBase64() toe, die byte-arrays direct codeert met een optie voor het URL-veilige alfabet. Dit artikel is de beslissingskaart: welk gereedschap voor welk werk, waar de scherpe randen zich verbergen (ze leiden allemaal terug naar dezelfde grens), en de concrete recepten voor de plekken waar je daadwerkelijk om Base64 zal worden gevraagd.

Je encoder kiezen

Er is niet langer één "de" encoder, en naar de verkeerde grijpen is hoe de klassieke bugs ontstaan. De tabel hieronder is de hele beslissingsboom:

Situatie Pak
Platte ASCII-tekst, eenmalige waarde btoa(text)
Echte tekst met accenten, emoji, CJK new TextEncoder().encode(text), daarna btoa of toBase64
Bytes al in een Uint8Array bytes.toBase64() in browsers van 2025+, elders de in chunks verwerkte btoa-brug
URLs, JWTs, bestandsnamen toBase64({ alphabet: 'base64url', omitPadding: true })
Oudere browsers of een gedeelde codebasis js-base64, of het klassieke TextEncoder + btoa-recept

Het patroon onder de tabel: btoa() leest alleen een-byte tekens, dus alles wat geen ASCII is, moet eerst een byte-array worden, en dat byte-array is waar de moderne API's omheen zijn gebouwd. Houd "tekst wordt bytes, bytes worden Base64" in je hoofd, en elk recept in dit artikel is dezelfde twee stappen met andere namen erop.

btoa en de Latin1-grens

btoa(stringToEncode) - binaire tekenreeks naar ASCII-tekenreeks - is de oorspronkelijke encoder, beschikbaar in elke browser die ertoe doet (Chrome 4, Firefox 1, Safari 3, IE 10 en nieuwer, alle worker-scopes, en Node vanaf versie 16). Zijn afspraak heeft één clausule, en die clausule is waar alles misloopt: elk teken in de invoer moet een codepunt tussen 0 en 255 hebben. De functie leest codepunten, geen UTF-8-bytes, dus "é" (codepunt 233) zeilt door, terwijl "你" (codepunt 20320) een DOMException met de naam InvalidCharacterError gooit voordat er een enkel teken is gecodeerd. De grens is niet "ASCII", het is niet "Unicode", het is precies 256, en het omvat de controletekens aan de onderkant - één NUL-byte coderen is legaal en betekenisvol, en dat is één van de redenen waarom de functie überhaupt bestaat.

Het volledige gedrag, regel voor regel:

Invoer Resultaat
"Hello, World!" "SGVsbG8sIFdvcmxkIQ==" - het schoolboekgeval
"" (lege tekenreeks) "" - niets erin, niets eruit
"\u0000" (NUL) "AA==" - controletekens zijn volwaardige burgers
"a\u00e9z" (é, codepunt 233) "Yel6" - het hele Latin1-bereik slaagt
"\u0100" (codepunt 256) gooit InvalidCharacterError - één stap voorbij de grens
"h\u4f60" (你, codepunt 20320) gooit InvalidCharacterError - en hetzelfde doen alle emoji's, want ze zitten allemaal ver boven 255

Twee praktische opmerkingen. Het foutbericht verschilt per engine - Firefox zegt "String contains an invalid character", Chrome zegt dat de tekenreeks "characters outside of the Latin1 range" bevat - dus vang je in elke defensieve code op op de naam van de exceptie. En de throw gebeurt bij het eerste overtredende teken, niet aan het eind: btoa codeert niet de helft van de tekenreeks en maakt dan excuses. Wil je het Latin1-gedrag bewust (een byte-tekenreeks coderen die opzettelijk uit codepunten 0-255 is gebouwd), dan doet de functie precies wat je vroeg, en is de tabel hierboven zijn hele personaliteit.

De bytes-brug

De vraag wordt dan: hoe komen echte data - de UTF-8-bytes van je tekst, de inhoud van een bestand, de uitvoer van een canvas - binnen in de invoer van btoa()? Het antwoord is de "bytes-brug": een JavaScript-tekenreeks waarin elk teken één byte-waarde bevat, dezelfde truc die de decoders produceren en die btoa natief begrijpt. De naïeve versie is een loop:

function bytesToBase64 (bytes) {
  let binary = '';
  for (let i = 0; i < bytes.length; i += 1) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

Correct, maar tekenreeks-concatenatie in een loop is traag voor grote bestanden, en de populaire snelkoppeling - String.fromCharCode.apply(null, bytes), die het hele array als argumenten in één aanroep invoedt - heeft een harde klip. Functieaanroepen hebben een beperking op het aantal argumenten, en die wordt ver voor je eerste megabyte bereikt:

const big = new Uint8Array(1000000);
btoa(String.fromCharCode.apply(null, big));
// RangeError in Firefox: "too many arguments provided for a function call"
// RangeError in Chrome: "Maximum call stack size exceeded"

De fix die meer uploadfuncties heeft gered dan enige andere enkele wijziging is de brug in chunks oversteken, enkele duizenden tekens tegelijk, en de resultaten samen te voegen:

function bytesToBase64Chunked (bytes) {
  const CHUNK = 0x8000;
  const parts = [];
  for (let i = 0; i < bytes.length; i += CHUNK) {
    parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK)));
  }
  return btoa(parts.join(''));
}

Elke chunk is klein genoeg om veilig toe te passen, subarray geeft een weergave zonder te kopiëren, en de join produceert exact dezelfde binaire tekenreeks die de loop had geproduceerd. Nu de tekstzijde van de munt. Voor elke echte tekst zet TextEncoder - de UTF-8-encoder van het platform, beschikbaar in Firefox 18, Chrome 38, Safari 10.1 en sindsdien overal - je tekenreeks om in eerlijke bytes voordat de brug zijn werk doet:

const bytes = new TextEncoder().encode('hello 你好');
const base64 = bytesToBase64Chunked(bytes);
console.log(base64); // "aGVsbG8g5L2g5aW9"

Die uitvoer is wat "hello 你好" op de draad echt is: zes ASCII-bytes plus zes UTF-8-bytes voor de twee Chinese tekens, allen in dezelfde afdrukbare vermomming. Is je tekst niet UTF-8 - en op het web is het dat meestal wel - dan heb je eerst de andere charset nodig, en dat betekent coderen ergens die die charset spreekt, meestal de server. TextEncoder weigert opzettelijk te gokken, en dat is terecht.

De snelroute van 2025: Uint8Array.toBase64

Heb je al een Uint8Array in handen, dan is de brug een omweg, want de nieuwe ECMAScript (ES2026)-feature codeert het array direct: bytes.toBase64(options). Het landde in Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 en Deno 2.5 - dezelfde Baseline 2025-golf als zijn decoderingszuster - en het accepteert twee opties die het de veelzijdigste encoder op het platform maken. De eerste is alphabet: "base64" (de standaard) of "base64url". De tweede is omitPadding: zet die op true en de afsluitende =-tekens worden weggehaald, wat de vorm is die de meeste URL-vriendelijke ontvangers willen. Iets anders als opties doorgeven gooit een TypeError, en dat is de API die beleefd is over je typo:

const bytes = new Uint8Array([251, 255]);
console.log(bytes.toBase64()); // "+/8="
console.log(bytes.toBase64({ omitPadding: true })); // "+/8"
console.log(bytes.toBase64({ alphabet: 'base64url' })); // "-_8="

Die twee bytes zijn gekozen om het alfabet zo veel mogelijk lastig te vallen: ze produceren een + en een / in standaardmodus, dus de laatste regel toont precies wat er verandert als je naar base64url schakelt. Prestaties zijn de stille bonus: op een recente Firefox kost het coderen van tien megabyte zo'n vijf millisecondes met toBase64, terwijl de tekenreeks-brugroute hierboven zo'n vijftien keer zo lang duurt, omdat die onderweg een reusachtige tussen-tekenreeks bouwt. In oudere browsers blijft de brug voor alles onder enkele megabytes prima bruikbaar - en de chunk-versie hierboven is degene die je wilt, om de redenen uit de vorige sectie.

URL-veilige uitvoer

Base64 heeft een eigen variant voor de plekken waar +, / en = schade veroorzaken, en die verdient een eigen sectie, omdat zoveel kapotte code gewoon standaard Base64 is dat tegen een URL aanliep. In een query string is + een spatie; in een pad is / een scheidingsteken; en = wil in sommige posities percent-encoding. Het URL-en-bestandsnaam-veilige alfabet uit RFC 4648, sectie 5 - base64url - vervangt die twee tekens door - en _, en omdat de datalengte aan de ontvangende kant meestal bekend is, staat het ook toe om de padding helemaal weg te laten. De uitvoer reist door URLSearchParams, padsegmenten, fragments en bestandsnamen zonder een enkel percent-teken.

Met de 2025-API is dit één object met opties:

const params = new URLSearchParams();
params.set('payload', bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(params.toString()); // "payload=-_8" - helemaal geen percent-encoding

In oudere browsers converteer je na het coderen met btoa. Twee replaces en een trim doen het hele werk:

function toUrlBase64 (base64) {
  return base64
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}
console.log(toUrlBase64(btoa('hi?/x'))); // "aGk_L3g"

Drie regels houden het kanaal schoon. Kies één alfabet per kanaal en blijf erbij - een waarde die + en - mengt, behoort tot geen enkele familie, en geen decoder raadt welke je bedoelde. Padding is een contract, geen voorstel: laat je het weg, dan moet de ontvanger gereed zijn voor een ongepadde waarde, en houd je het aan, dan moet de ontvanger er niet over struikelen (browsers zijn tolerant, sommige JSON-schema's niet). En onthoud dat de ruil omkeerbaar en verliesloos is - - en _ komen op dezelfde 62e en 63e alfabetposities uit waar + en / staan, dus er gaat niets verloren aan het kiezen van het vriendelijke paar.

Beelden laten reizen: data URLs

De oudste en zichtbaarste toepassing van Base64 in de browser is de data URL: data:, een optionele media type, een optionele ;base64-flag, een komma, en dan de payload. Tekst-payloads zijn percent-encoded; binaire payloads - afbeeldingen, fonts, audio - zijn Base64, en de browser rendert ze met nul HTTP-verzoeken. Voor een afbeeldingsbestand dat de gebruiker zojuist heeft gekozen, doet FileReader het coderen voor je en reikt het de afgemaakte URL aan:

const reader = new FileReader();
reader.onload = () => {
  console.log(reader.result); // "data:image/png;base64,iVBORw0KGgo..."
  imageElement.src = reader.result;
};
reader.readAsDataURL(file);

Het resultaat is een kant-en-klaar src, een waarde die je in localStorage kunt bewaren of in een JSON-lichaam kunt sturen. Staat de afbeelding in plaats daarvan op een canvas - een screenshot, een verwerkte foto, een gegenereerde grafiek - dan doet canvas.toDataURL() dit werk al sinds de vroegste browserreleases, en laat het je zelfs het formaat kiezen en, voor formaten met verlies, de kwaliteit:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(photo, 0, 0);
const pngUrl = canvas.toDataURL('image/png');
const jpegUrl = canvas.toDataURL('image/jpeg', 0.8);

Drie valkuilen om rekening mee te houden. Eerst, de tainted-canvas-regel: tekende je een cross-origin afbeelding op de canvas zonder CORS-toestemming, dan gooit elke poging om pixels terug te lezen - inclusief toDataURL - een SecurityError. De fix is de afbeelding laden met crossOrigin = 'anonymous' en ervoor zorgen dat de server de juiste headers stuurt. Tweede, het kwaliteit-argument wordt voor PNG genegeerd en heeft alleen betekenis voor JPEG (en WebP) - een veelvoorkomende bron van "waarom is mijn PNG groter". Derde, en de grootste: de payload is zo'n 33 procent groter dan het bestand, en hij zit in de pagina als een tekenreeks. Voor afbeeldingen die de browser nooit verlaten, is er een gratis alternatief - een object URL, die de Blob inpakt zonder hem in te coderen:

const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
URL.revokeObjectURL(objectUrl); // als je er klaar mee bent

De taakverdeling die daaruit voortvalt: object URLs voor alles wat op de pagina blijft, data URLs voor alles dat gekopieerd, bewaard of als tekst gestuurd moet worden. Beide zijn volwaardig; ze lossen gewoon verschillende problemen op.

Een JWT bouwen en tekenen

Genereer je tokens in de browser - voor een zelfgehoste authenticatie-flow, een demo, of een serverless front end - dan is het compacte JWS-formaat drie base64url-segmenten: header, payload, signatuur, overal zonder padding. De Web Crypto API verzorgt het tekenen; de codering is exact de URL-veilige uitvoer van twee secties geleden:

const encoder = new TextEncoder();
const segment = (bytes) =>
  bytes.toBase64({ alphabet: 'base64url', omitPadding: true });
const header = segment(encoder.encode(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
const payload = segment(encoder.encode(JSON.stringify({ sub: '1234567890', name: 'John Doe' })));
const key = await crypto.subtle.importKey(
  'raw',
  encoder.encode('shared-secret'),
  { name: 'HMAC', hash: 'SHA-256' },
  false,
  ['sign']
);
const signature = segment(
  new Uint8Array(
    await crypto.subtle.sign('HMAC', key, encoder.encode(header + '.' + payload))
  )
);
const token = header + '.' + payload + '.' + signature;

Twee details wegen zwaarder dan het loodwerk. De signatuur dekt exact header + '.' + payload - de rauwe segmenten, niet de JSON - dus elke wijziging aan welke van beide delen ook het token ongeldig maakt, en dat is het hele punt. En crypto.subtle.sign geeft een rauwe ArrayBuffer terug, vandaar dat je het in één regel in een Uint8Array verpakt voordat de segment-codering hem ziet. Voor RSA-gebaseerde tokens is de flow identiek met RS256 en een sleutelpaar, en exporteer je een openbare sleutel als JWK (crypto.subtle.exportKey('jwk', key)), dan komen de numerieke leden - n, e, en voor privésleutels d, p, q - vanzelf als ongepadde base64url uit. De veiligheidsvoorzieningen zijn dezelfde als bij elke token: een alg: "none"-header is een verzoek om verificatie over te slaan, tijdclaims (exp, nbf) moeten worden afgedwongen, en een server die voor hetzelfde publiek zowel HMAC als RSA accepteert, opent de deur van de klassieke key-confusion. Codeer correct, teken correct, en verifieer aan de ontvangende kant.

Authenticatie-headers

De eenvoudigste authenticatiemethode op het web is ook de meest leerzame over wat Base64 wél en níet is. HTTP Basic stuurt Authorization: Basic gevolgd door de Base64 van username:password - één aanroep, geen bytes-brug nodig, want gebruikersnamen en wachtwoorden zijn (hopelijk) platte tekst:

const credentials = btoa('alice:secret123');
fetch('/api/me', {
  headers: { Authorization: 'Basic ' + credentials }
});
// Authorization: Basic YWxpY2U6c2VjcmV0MTIz

En hier is de les die op één regel past: Base64 is geen encryptie. De header hierboven staat op één atob-aanroep van alice:secret123 verwijderd - voor de aanvaller en voor iedereen die de logs leest - dus Basic auth is alleen aanvaardbaar via HTTPS, waar het transport de echte bescherming is en Base64 alleen de opmaak. Voor alles met een langere levensduur dan één verzoek, kies dan voor token-gebaseerde schemes: een Bearer-token is ook een enkele header, maar het is een willekeurige waarde waarvan het geheim nooit in de header zelf hoeft te reizen, en het kan worden ingetrokken. Het coderingsverschil tussen de twee is triviaal - allebei btoa of platte tekst - maar het veiligheidsverschil niet, en dat moet je met opzet maken.

Bestanden erin, tekst eruit

Uploads zijn waar de 33-procent-belasting in echt geld wordt uitgesproken, want het bestand is meestal het grootste ding op de pagina. Er zijn twee wegen, en de eerste is de weg die je standaard zou moeten nemen: multipart-formdata. FormData draagt het bestand als ruwe bytes in een standaardlichaam, waarbij de browser het afbakenen voor zijn rekening neemt, en er is nergens in het beeld Base64 - geen groottenbelasting, geen tussen-tekenreeks, en de bytes stromen naar de server zoals ze gelezen worden:

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

De tweede weg is voor de API's die erop staan dat het JSON-lichaam het bestand als tekenreeks bevat - sommige serverless functies, sommige mobiele backends, sommige legacy-services. Daar is de codering één regel per bestand, en de kost is exact wat de belasting zegt: een bestand van 5 megabyte wordt een tekenreeks van 6,7 megabyte, die dan wordt geserialiseerd naar JSON, en die dan wordt gestuurd. Prima voor een foto, pijnlijk voor een video:

const bytes = new Uint8Array(await file.arrayBuffer());
const body = JSON.stringify({
  name: file.name,
  content: bytes.toBase64()
});
await fetch('/api/upload-json', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body
});

Voor grote bestanden op die weg, bouw dan niet één reusachtige tekenreeks in één aanroep - bouw het in stukken, waarbij elk stuk een veelvoud van drie bytes is. Die uitlijning is wat de truc legaal maakt: een veelvoud van drie bytes codeert naar een schoon veelvoud van vier tekens zonder padding, dus onafhankelijk gecodeerde slices voegen zich samen tot exact de codering van het hele bestand, en alleen het laatste stuk draagt ooit padding:

async function encodeLargeFile (file) {
  const bytes = new Uint8Array(await file.arrayBuffer());
  const SLICE = 3 * 1000 * 1000;
  const parts = [];
  for (let i = 0; i < bytes.length; i += SLICE) {
    parts.push(bytes.subarray(i, i + SLICE).toBase64());
  }
  return parts.join('');
}

Hetzelfde uitlijn-idee is de reden waarom je een Base64-tekenreeks nooit op een willekeurig punt zou moeten splitsen en verwachten dat de stukken apart decoderen - een driebytengroep is het atoom, en een snede in het midden laat een hangend fragment achter. Downloads zijn het spiegelbeeld: voor een gegenereerd bestand kan een klein bestandje via een data URL op een downloadlink naar buiten, maar voor iets substantieels is Blob plus object URL de gezonde route, omdat de browser de hele payload in de eerste plaats nooit als tekenreeks hoeft te dragen.

State opslaan en delen

Nog twee kanalen die alleen tekst dragen, waar Base64 echt werk doet. Het eerste is opslag: localStorage en sessionStorage bevatten tekenreeksen, dus gestructureerde of binaire data wordt gecodeerd voordat het erin gaat. De rondreis is één codering en één decoding, en het is de moeite waard om beide kanten samen te zien, want een opslagbug is bijna altijd een charset-mismatch tussen de twee:

const state = { theme: 'dark', draft: 'hello' };
const packed = new TextEncoder().encode(JSON.stringify(state));
localStorage.setItem('app-state', new Uint8Array(packed).toBase64());
const raw = atob(localStorage.getItem('app-state'));
const bytes = Uint8Array.from(raw, (c) => c.codePointAt(0));
const state = JSON.parse(new TextDecoder().decode(bytes));

Maar budgeteer het wel goed: de origin krijgt zo'n 5 megabyte localStorage, je opgeslagen tekenreeks is 33 procent vetter dan de data, en zolang de pagina open is, leeft de tekenreeks ook in geheugen als UTF-16 - nog eens het dubbele van zijn lengte. Een asset van 3 megabyte is 4 megabyte opslag en 8 megabyte geheugen, en zo wordt een "klein" feature een quota-fout. Het tweede kanaal is de URL zelf: deellinks, deeplinks en OAuth state willen allemaal gestructureerde data op een plek die copy-paste overleeft. Het recept is compacte state, JSON, dan base64url zonder padding, zodat de waarde helemaal geen percent-encoding nodig heeft - en houd de hele URL onder een paar duizend tekens, want daar beginnen oudere clients, proxys en logboeken zenuwachtig te worden.

E-mail en MIME

Base64 is ouder dan het web, en zijn thuisbasis is e-mail. MIME-bijlagen met Content-Transfer-Encoding: base64 zijn hoe een binair bestand mee reist in een tekstprotocol, en de conventie die volgde uit de oude regelbeperking van 76 tekens van het berichtformaat is de moeite van het kennen waard: wikkel de gecodeerde body om op 76 tekens per regel. De browser kan geen SMTP verzenden, maar hij doet twee e-mailtaken - MIME-lichamen bouwen die een backend-relay zal versturen, en de bijlagen van berichten die hij ontvangt weergeven - en beide raken de codering. Het wikkelen zelf is een twee-regelfunctie, en de volgorde van handelen telt: codeer eerst en wikkel daarna, want btoa gooit geen uitzondering om een regeleinde in zijn invoer - het codeert het regeleinde als een payload-byte, en je regeleinden belanden in de uitvoer:

function wrapForMime (base64, width) {
  const w = width || 76;
  return base64.match(new RegExp('.{1,' + w + '}', 'g')).join('\r\n');
}

De ontvangende kant is de makkelijke: atob slaat ASCII-witte ruimtes over als onderdeel van zijn standaardgedrag, dus een omgewikkelde MIME-body decodeert exact zoals hij aankwam, regeleinden en alles, zonder dat je hem eerst weer tot één regel hoeft samen te voegen. Bouw je een webmailclient of een bijlagenpicker, dan is die ene asymmetrie - de encoder moet schone regels produceren, de decoder geeft er niets om - het hele MIME-verhaal in één zin.

Wanneer je naar een bibliotheek moet grijpen

Met de native gereedschappen hierboven is een bibliotheek zelden nodig, en het eerlijke advies is: ga standaard uit van het platform, en voeg een pakket toe alleen wanneer een echte eis ernaar wijst. De drie die daadwerkelijk in codebases opduiken:

js-base64 (npm install js-base64) is de alleskunner: een klein, zuiver-JavaScript transcoder dat UTF-8-tekenreeksen als volwaardige burgers behandelt - Base64.encode op een CJK-tekenreeks doet de UTF-8-dans voor je - en, net zo nuttig voor decoderen als voor coderen, beide alfabetten in decode accepteert en een isValid-check meelevert. Het is het juiste antwoord wanneer je op browsers gericht bent waar de 2025-API's ontbreken en je één import wilt die tekenreeksen én bytes dekt:

import { Base64 } from 'js-base64';
const encoded = Base64.encode('小飼弾'); // "5bCP6aO85by+" - UTF-8 wordt voor je afgehandeld
const decoded = Base64.decode('5bCP6aO85by-'); // leest standaard en URL-safe allebei
const valid = Base64.isValid(encoded); // true

base64-js is de bytes-gerichte variant: fromByteArray en toByteArray op Uint8Array's, geen afhankelijkheden, het werkpaard van het oude browserify-ecosysteem en nog steeds een prima keuze wanneer je code in typed arrays leeft en je wilt dat de codering een pure functie is van bytes. En als je reden om een bibliotheek te willen "ik hou van de 2025-API maar ik kan 2025-browsers niet vereisen" is, dan is het antwoord helemaal geen Base64-pakket, maar een polyfill: core-js (en het Babel-preset dat het meeneemt) implementeert Uint8Array.fromBase64 en de anderen, zodat je de nieuwe-stijl code één keer schrijft en de shim de kloof op oudere engines laat overbruggen. Kies per beperking - oude browsers, tekenreeks-gemak, of bytes-pureheid - niet per gewoonte.

Valkuilen die developers uren kostten

  • btoa aanroepen op een tekenreeks met een teken boven codepunt 255. Het gooit een exceptie, het verbastert niet, en het stopt bij de eerste overtreding. De fix is altijd dezelfde: eerst TextEncoder, daarna de brug.
  • De fromCharCode.apply-klip bij grote arrays. Een miljoen argumenten is een RangeError in beide grote engines. Maak de brug in chunks, of ga over naar toBase64.
  • De groottenbelasting vergeten waar het het hardst bijt: opslag. Een bestand in localStorage is 33 procent groter dan het bestand, en het quota is per origin, gedeeld met alles wat je app nog opslaat.
  • Standaard Base64 dat tegen een query string aanloopt. De + komt binnen als spatie, de / breekt het pad, en de bugrapporten zeggen "de API is labiel". URL-veilige uitvoer, geen padding, en het hele genre bugs verdwijnt.
  • Inconsistent padding tussen services. Eén gateway houdt de =, een andere haalt hem weg, een derde voegt hem terug toe. De ontvanger moet gereed zijn voor beide vormen, en het contract zou moeten zeggen welke van de twee de officiële is.
  • Base64 behandelen als een slot. Het is een serialisatieformaat, één functieaanroep van platte tekst, en "gecodeerd met Base64" in een security review is een bevinding, geen controle.
  • Binaire tekenreeksen als geheugenmodel. Een gedecodeerde of gecodeerde megabyte reist in UTF-16 met twee megabyte mee; een Uint8Array houdt het met één vast. Voor grote payloads, houd de bytes van begin tot eind in typed arrays.
  • Dubbele codering. Een waarde die al Base64 was, wordt nog eens gecodeerd, en de consument decodeert één keer en krijgt een tekenreeks van letters in plaats van data. Twijfel je, controleer dan voordat je inpakt - een tekenreeks die al in het alfabet zit met geldige padding is een slecht teken.
  • Een JWT-payload vertrouwen omdat hij schoon decodeert. Decodeerbaarheid is geen echtheid. Verifieer de signatuur met de juiste sleutel en het juiste algoritme voordat je een enkele claim leest.

Prestaties: wat een miljoen bytes kost

Base64 in de browser is goedkoop waar het duur was, en het budget heeft nu drie posten in plaats van één. CPU: op een recente Firefox codeert Uint8Array.toBase64 tien megabyte in zo'n vijf millisecondes, terwijl de in chunks verwerkte btoa-brug zo'n vijftien keer zo lang duurt - niet omdat btoa traag is, maar omdat de brug onderweg een reusachtige tussen-tekenreeks bouwt. Is je coderingsbudget in millisecondes, gebruik dan de native methode; codeer je een configuratieobject van 2 kilobyte, dan zitten beide onder de perceptiedrempel. Bandbreedte: dit is de permanente belasting - elke byte die je codeert kost 1,33 bytes op de draad, plus wat het transport aan framen toevoegt. Meet de overdracht voordat je de codering "optimaliseert". Geheugen: de gecodeerde tekenreeks is de grootste tijdelijke toewijzing die je maakt, en voor een bestand van 5 megabyte is het een tekenreeks van 6,7 megabyte, of zo'n 13,4 megabyte UTF-16-geheugen zolang de pagina hem vasthoudt. De praktische gevolgen vallen uit de rekenkunde: snij grote coderingen in stukken zodat geen enkele tekenreeks reusachtig wordt, laat de tussen-bytes zo snel mogelijk gaan zodra de tekenreeks bestaat, kies dan voor object URLs en multipart wanneer de bytes nooit afdrukbaar hoeven te zijn, en verplaats werk van meerdere megabytes naar een Web Worker als de hoofdthread soepel moet blijven scrollen. Het formaat is bijna vier decennia oud; het platform heeft het eindelijk ingehaald.

Hoe browsers coderen leerden

De encoder heeft een geschiedenis, en die legt uit welke relicten je gaat erven. btoa - "binair naar ASCII", de naam is letterlijk, en atob is dezelfde woorden gewoon omgedraaid - werd in 2011 in de HTML-specificatie geschreven, achteraf ontdekt uit de browsers die het al hadden uitgebracht: Firefox sinds 2004, Safari 3, Chrome 4. Internet Explorer, kenmerkend genoeg, sloeg beide functies over tot versie 10 in 2012, en die ene afwezigheid is de reden dat een decennium JavaScript vol zit met handgemaakte Base64-tabellen en één specifieke incantatie voor Unicode: btoa(unescape(encodeURIComponent(str))). Het werkte - encodeURIComponent produceert percent-geëscapte UTF-8, en unescape maakte daar een byte-tekenreeks van - maar het was gebouwd op unescape(), het ene van het paar dat de taal had afgeschaft, en het overleefde jaren in browsercode door pure inertie. De principiele fix kwam met de Encoding-standaard: TextEncoder en TextDecoder, in Firefox 18 (2013), Chrome 38 (2014), Safari 10.1 (2017), en in geen enkele versie van IE - weer een IE-put, weer een decennium aan workarounds. Node.js vertelt de server-kant van het verhaal: het had Buffer met Base64 vanaf de eerste dag, maar atob en btoa als globalen pas vanaf versie 16 in 2021, en daarvoor droegen twee kleine npm-shims de last. En toen, verspreid over eind 2024 en 2025, bracht de taal zelf Base64 uit - Uint8Array.toBase64 en de anderen in Firefox 133 (november 2024), Safari 18.2 (december 2024), Chrome 140 (september 2025) en Node 25 (oktober 2025) - en het feature werd Baseline 2025 gemarkeerd - dezelfde feature-set die het platform twintig jaar lang met helpers had nabootst, nu standaard. De leuke trivia's aan het eind van het artikel gaan grotendeels over hoe lang elk onderdeel erover deed om aan te komen.

Wist je dat...?

  • De functienamen zijn samen een uitdrukking: btoa is "binair naar ASCII" en atob is "ASCII naar binair". De richting zit in de naam, en daarom is het paar al sinds de 2000s zichzelf documenterend.
  • De meest gecodeerde tekenreeks in de geschiedenis van de informatica is waarschijnlijk "hello": btoa('hello') is aGVsbG8=, de uitvoer van elke tutorial, test suite en interview-witbord op de planeet.
  • Elke geldige Base64-tekenreeks heeft een lengte die een veelvoud van vier is, padding inbegrepen. De =-tekens zijn een vingerafdruk: één ervan betekent dat de laatste groep twee bytes bevatte, twee ervan betekent dat het er één bevatte.
  • De regelomwikkeling op 76 tekens in MIME en in de meeste command-line tools is een erfenis van het e-mailtijdperk, toen de regellengte van het berichtformaat de beperking bepaalde. Het getal heeft drie decennia van steeds sneller alles overleefd.
  • "Data URI" is een gepensioneerde naam. De WHATWG hernoemde het naar "data URL" tijdens de grote URI-naar-URL-harmonisatie, waardoor specificaties, blogposts en pakketnamen het in dezelfde alinea allemaal anders spellen.
  • btoa('') geeft '' terug: een lege invoer produceert een lege uitvoer, geen padding, geen speciaal geval - de enige Base64-tekenreeks met nul tekens (zijn lengte, 0, is nog steeds een veelvoud van vier).
  • Een canvas kan met toDataURL een foto omzetten in een data URL - een vermogen dat al bestaat sinds IE 9, Firefox 2 en Safari 4, ouder dan het grootste deel van het webplatform dat we als "modern" beschouwen - en hem via een <img>-tag en een FileReader weer de reis terugsturen.
  • De WebSocket-handshake codeert SHA-1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11) in Base64, en de GUID is een vaste constante in de RFC die precies zo is gekozen dat geen gewone HTTP-server de handshake per ongeluk kon voltooien.

Waar je nu naartoe kunt

Dus past het hele ambacht van coderen in de browser op één pagina: btoa voor de platte, een-byte gevallen waarvoor het is geboren; TextEncoder plus de in chunks verwerkte brug voor echte tekst en bestanden in elke browser; Uint8Array.toBase64 met zijn alfabet- en paddingopties voor de moderne, directe route; en de URL-veilige variant, met of zonder padding, voor alles wat in een URL zal leven. De rest is oordeelsvermogen: ken de 33-procent-belasting voordat je haar betaalt, houd de bytes in typed arrays zolang ze groot zijn, codeer eerst en wikkel daarna, en noem een serialisatieformaat nooit een slot. Kan het kanaal ruwe bytes dragen, pak dan de bytes - Base64 is voor de wegen die alleen afdrukbare tekst toelaten, en nu weet je exact hoe je de tol betaalt.

De andere helft van de reis - zo'n tekenreeks ontvangen en er weer de bytes, tekst en betekenis uit halen - wordt in detail behandeld in de daarbij behorende gids over Base64-decoderen in JavaScript, hieronder gelinkt.

Laatst bijgewerkt: 2026-10-06

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