Devi lavorare con il formato Base64? Allora questo sito è perfetto per te! Usa il nostro praticissimo strumento online per codificare o decodificare i tuoi dati.

Decodifica Base64 in JavaScript/Browser: una guida completa

Arriva in una dozzina di travestimenti diversi: un JWT infilato nell'intestazione Authorization, un oggetto binario image/png dentro una risposta JSON, un valore Sec-WebSocket-Accept in un log di negoziazione, un allegato email avvolto in MIME, un valore che il tuo backend ha infilato con educazione in una stringa di query. La stringa in sé ha sempre lo stesso aspetto: una lunga fila di lettere e cifre, ogni tanto un + o uno /, e magari un = o due alla fine. Se la home page di questo sito ti ha insegnato cos'è il Base64 - quattro caratteri stampabili che fanno da sostituti per ogni tre byte, con il riempimento = per chiudere l'ultimo gruppo - allora questo articolo parla della parte che fai davvero nel codice: riportare quei caratteri in byte, e i byte di nuovo in significato, usando solo ciò che il browser distribuisce già.

Due rapide regole di base prima di partire. Prima, la decodifica è la direzione che rimpicciolisce: per ogni quattro caratteri che leggi, tre byte escono, quindi l'output occupa sempre meno memoria dell'input. Secondo, una stringa Base64 decodificata non è automaticamente testo. Sono byte, e quei byte possono rivelarsi UTF-8, Windows-1252, l'intestazione di un PNG, o una firma criptografica. Il bug più comune in assoluto nel codice Base64 è dimenticare quali di quelli hai tra le mani, quindi le sezioni qui sotto sono organizzate attorno a quella domanda.

I tre livelli della decodifica

I browser moderni ti danno tre livelli nativi, e la buona notizia è che non serve mai nessun pacchetto. Ognuno risponde a una domanda leggermente diversa, e scegliere quello giusto ti risparmia un sacco di snippet copiati da Stack Overflow:

Livello Cosa mangia Cosa ti passa Personalità Disponibilità
atob() stringa Base64 standard una "stringa binaria" (un byte per carattere) molto tollerante: salta gli spazi bianchi ASCII, accetta il riempimento mancante ogni browser dal 2000 in poi, IE 10+, Node 16+
TextDecoder byte (Uint8Array) testo JavaScript leggibile configurabile: etichetta per la codifica dei caratteri, flag fatal per il rigore Firefox 18, Chrome 38, Safari 10.1 e successivi (mai in IE)
Uint8Array.fromBase64() stringa Base64 più opzioni un vero Uint8Array rigorosa con le manopole: alfabeto e gestione dell'ultimo blocco Baseline 2025: Chrome 140, Firefox 133, Safari 18.2, Node 25

La forma dell'intero articolo deriva da quella tabella. atob() è il cavallo da lavoro che incontrerai ovunque, incluso nel codice vecchio. TextDecoder è il ponte dai byte alle parole. E Uint8Array.fromBase64() è l'aggiornamento 2025 che salta del tutto il passaggio intermedio quando, in fondo, volevi solo i byte.

atob: veloce, tollerante e molto antica

L'intero contratto sta in una riga: atob(encodedData). Prende una stringa codificata in Base64 e restituisce una "stringa binaria": una stringa JavaScript normale in cui ogni carattere custodisce esattamente un byte decodificato, un punto di codice da 0 a 255. Quel tipo di restituzione conta, perché non è la stessa cosa di un testo leggibile (ne parliamo più sotto). La funzione in sé è velocissima, ed è in circolazione da moltissimo tempo: Chrome 4, Firefox 1, Safari 3, e - questo è il caso che più persone ricordano - Internet Explorer solo dalla versione 10 in poi, ed è per questo che il codice scritto prima del 2012 è pieno di tabelle Base64 fatte a mano.

Ciò che rende atob() piacevole è quanto perdona prima di arrendersi. Lo standard HTML del WHATWG dice di ignorare tutti gli spazi bianchi ASCII - spazio, tabulazione, avanzamento di riga, avanzamento di pagina, ritorno a carrello - prima di decodificare, quindi una stringa avvolta in MIME con a capo ogni 76 caratteri si decodifica senza che tu debba pulirla. Anche il riempimento mancante viene perdonato. Ma nel momento in cui vede un carattere fuori dall'alfabeto, o una lunghezza che non potrebbe mai essere valida, lancia un'eccezione DOMException di nome InvalidCharacterError. Nessuna spazzatura silenziosa, nessun risultato parziale.

Ecco il rapporto dei danni, riga per riga:

Input Risultato
"SGVsbG8sIFdvcmxkIQ==" "Hello, World!" - il caso da manuale
"aGVsbG8" (senza riempimento) "hello" - un = mancante viene perdonato
"SGVs\nbG8s\nIFdvcmxkIQ==" (righe avvolte) "Hello, World!" - prima si saltano gli spazi bianchi ASCII
"" (stringa vuota) "" - l'input vuoto è valido e fa l'andata e il ritorno senza problemi
"A" (un carattere avanzato) lancia InvalidCharacterError - un carattere non può codificare nulla
"Zm9vYmFy!" (un ! intruso) lancia InvalidCharacterError - fuori dall'alfabeto
"ZGFua29nYWk-" (carattere URL-safe mescolato dentro) lancia InvalidCharacterError - i due alfabeti non vanno mescolati
"Zm9v====" (riempimento in eccesso) lancia InvalidCharacterError - al massimo due = alla fine

Una nota pratica: il messaggio di errore in sé varia da motore a motore (Firefox dice "String contains an invalid character", Chrome dice che la stringa "contains characters outside of the Latin1 range" per un input non Latin1 o "is not correctly encoded" per un base64 non valido), quindi cattura in base al nome dell'eccezione, non al testo del messaggio.

Dai byte grezzi al testo vero

Quel tipo di restituzione, la "stringa binaria", merita di fermarci un momento, perché è alla base della maggior parte delle confusione sulla decodifica. Le stringhe JavaScript sono UTF-16, quindi atob() ti passa una stringa i cui caratteri sono valori di byte, non glifi leggibili. Se il tuo carico utile era la codifica UTF-8 del testo "hello 你好", stampando il risultato direttamente ottieni un groviglio di caratteri incomprensibili. La correzione è una decodifica in due passi: da Base64 a byte, poi da byte a testo.

Prima il passo da Base64 a byte. Questo piccolo ausiliario è la ricetta classica e vale la pena tenerlo in tasca, perché è il pezzo portante della maggior parte degli esempi di questo articolo:

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

Poi il passo da byte a testo, con TextDecoder. Per UTF-8 (il predefinito, e la scelta giusta per JSON, carichi utili JWT e la maggior parte dei dati web) la chiamata è una riga sola:

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

A che servono due passi? Perché atob() non sa nemmeno in quale codifica di caratteri sono stati prodotti i byte. È un puro convertitore di bit. TextDecoder è il componente che interpreta i byte come codifica di caratteri, e accetta un'etichetta per il lavoro: utf-8, windows-1252, iso-8859-1, utf-16le, più circa 220 altre etichette. I dati usciti da un'applicazione degli anni Novanta di solito sono Windows-1252, e basta un argomento del costruttore:

const decoder = new TextDecoder('windows-1252');
const text = decoder.decode(bytes); // stessi byte, interpretazione diversa

Il costruttore di TextDecoder accetta anche un flag fatal, e vale la pena impostarlo a true ogni volta che il testo decodificato alimenta qualcosa di importante. Di predefinito il decoder è tollerante: le sequenze di byte non valide vengono sostituite in silenzio con il carattere di sostituzione Unicode, U+FFFD, e nessuno te lo dice. Con fatal: true, lo stesso danno lancia un TypeError invece di nascondersi:

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

È uno di quegli interruttori che nelle documentazioni sembra banale e in produzione sembra un incidente ai dati. Se il tuo input viene fornito dall'utente o dalla rete, decodifica in modo rigoroso e gestisci l'errore di proposito.

L'input sicuro per URL richiede una deviazione

Una variante del Base64 merita una sezione tutta sua, perché appare in continuazione nel mondo reale e atob() non la legge. È l'alfabeto sicuro per URL e nomi di file della sezione 5 di RFC 4648, di solito chiamato base64url: gli stessi 64 caratteri, eccetto che + e / vengono sostituiti da - e _, e il riempimento = viene spesso omesso, dato che la lunghezza dei dati è nota implicitamente. Lo scambio esiste per una ragione concreta: in un URL, + significa spazio e / avvia un segmento di percorso, quindi l'alfabeto standard dovrebbe essere codificato in forma percentuale carattere per carattere. Il base64url viaggia senza intoppi nelle stringhe di query, nei segmenti di percorso, nei frammenti e nei nomi di file.

Il problema è che i due alfabeti non sono intercambiabili, e atob() parla solo quello standard. Passagli un - o un _ e ottieni InvalidCharacterError. Hai due opzioni pulite.

Opzione uno, che funziona ovunque: converti l'alfabeto e ripristina il riempimento prima di chiamare atob():

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"

L'espressione (4 - (s.length % 4)) % 4 è l'intero trucco: calcola quanti caratteri = servirebbero a una stringa con il riempimento corretto di quella lunghezza, da zero a due.

Opzione due, nei browser 2025+: il nuovo decoder nativo accetta l'alfabeto come opzione, quindi nessuna chirurgia sulle stringhe:

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

Due regole ti tengono lontano dai guai. Non mescolare mai alfabeti dentro un singolo valore - un decoder che vede sia un + sia un - non ha modo di sapere a quale famiglia appartiene ciò che legge, e il comportamento corretto secondo la specifica è fallire. E metti d'accordo l'altro lato della linea sul fatto che il riempimento ci sia o no: ometterlo è lecito per base64url, quindi un ricevente deve essere pronto per entrambe le forme. atob() lo è già; le opzioni native qui sotto ti danno una manopola per questo.

La scorciatoia 2025: Uint8Array.fromBase64

Se guardi indietro verso l'ausiliario base64ToBytes, noterai che fa due cose: decodifica il Base64, poi copia i caratteri in un array di byte uno per uno in JavaScript. Quel ciclo di copia è la parte lenta ed evitabile, ed è esattamente ciò che il nuovo metodo ECMAScript elimina. Uint8Array.fromBase64(string, options) va dritto dalla stringa codificata a un array di byte, ed è distribuito in Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 e Deno 2.5 - la prima funzionalità della piattaforma JavaScript del genere ad arrivare, marchiata Baseline Newly available nel programma Baseline dei fornitori di browser.

L'oggetto di opzioni ha due manopole. La prima è alphabet: "base64" (il predefinito) o "base64url". La seconda è lastChunkHandling, che controlla cosa succede all'ultimo gruppo parziale di caratteri:

Modalità Regola per l'ultimo blocco
"loose" (predefinita) due o tre caratteri, oppure quattro con riempimento; i bit in eccesso avanzati vengono ignorati
"strict" esattamente quattro caratteri (riempimento solo dove la lunghezza lo richiede), e i bit in eccesso devono essere tutti zero
"stop-before-partial" vengono decodificati solo i gruppi completi di quattro caratteri; la coda parziale resta non letta

Come atob(), il metodo ignora gli spazi bianchi ASCII nell'input, quindi le righe avvolte vanno bene. A differenza di atob(), ha un'opinione su tutto il resto: un carattere fuori dall'alfabeto selezionato, o un ultimo blocco che viola la modalità scelta, lanciano un SyntaxError; passare qualcosa che non è una stringa lancia un TypeError. Ecco la modalità strict all'opera, che rifiuta un blocco con il riempimento mancante:

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

Le prestazioni sono l'altra ragione per preferirlo. Su un Firefox recente, sulla macchina dell'autore, decodificare un carico utile da 10 megabyte richiede millisecondi a una cifra con fromBase64, mentre il classico atob più la mappatura byte-per-carattere richiede circa venti volte di più, perché la parte lenta è il ciclo a livello JavaScript, non la matematica del Base64. Se i tuoi dati sono byte, salta la stringa del tutto.

Per i browser più vecchi, la situazione è semplice: tieni l'ausiliario base64ToBytes qui sopra, oppure incorpora un piccolo polyfill (core-js e il pacchetto es-arraybuffer-base64 del progetto es-shims ne distribuiscono entrambi uno per fromBase64) se vuoi scrivere codice in stile nuovo dappertutto. L'API è stabile - ora è nella specifica ECMAScript - quindi tutto ciò che scrivi contro di essa non sarà deprecato.

Leggere un JWT

La "stringa misteriosa" più comune nei log delle applicazioni è un JSON Web Token: tre segmenti separati da punti, header.payload.signature, dei quali i primi due sono oggetti JSON codificati in base64url. Decodificarne uno è una faccenda di cinque righe, ed è il riscaldamento perfetto per tutto quello che abbiamo visto finora:

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"

Ora la parte che i principianti saltano e i sistemi di produzione imparano a proprie spese: il carico utile non viene verificato dal fatto di essere decodificabile. Qualsiasi persona può scrivere un JWT con il carico utile che vuole; è il segmento di firma a legarlo a un segreto. Verificare un token HS256 nel browser usa la Web Crypto API, che ha bisogno della firma in byte - un'altra ragione per cui l'ausiliario da segmento a byte si guadagna lo stipendio:

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 solo se la firma corrisponde al segreto

Tre inciampi meritano di essere nominati. Primo, controlla l'intestazione prima di verificare: un token che dichiara alg: "none" ti chiede di fidarti del carico utile senza firma, e codice ingenuo è stato ingannato facendolo esattamente. Secondo, onora le dichiarazioni di tempo - exp, nbf, iat - dopo la verifica, non prima. Terzo, il classico attacco di confusione delle chiavi: un server configurato per RS256 ma che accetta anche HS256 lascia che un attaccante firmi token con la chiave pubblica (che è pubblica di proposito) usata come segreto HMAC. In breve: decodifica liberamente, non fidarti di nulla, verifica tutto.

Aprire i data URL

Un data URL incorpora un file intero dentro un URL: data:, un tipo multimediale opzionale, un flag ;base64 opzionale, una virgola, poi il carico utile. I carichi utili di testo sono codificati in forma percentuale, quelli binari sono Base64, e il browser li renderizza senza alcuna richiesta HTTP - né fetch, né andata e ritorno al server, nulla da mettere in cache. Il browser tratta ogni data URL come un'origine opaca e unica, ed è anche per questo che sono un vettore preferito per il contenuto insidioso: un documento data:text/html aperto in un iframe esegue i suoi script, e una Content-Security-Policy restrittiva può bloccare i data URL del tutto. Tieni a mente la tua CSP se cominci a passarli a markup controllato dall'utente.

Decodificarne uno è in gran parte chirurgia sulle stringhe, poi la stessa linea di byte di prima:

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

Il pezzo meta ti dice il tipo multimediale (image/png qui, con il marcatore ;base64 che conferma che il carico utile è Base64). Una volta che il carico utile è un Blob, vale tutto il normale: un URL di oggetto per un <img>, un link di download, o un POST al server. L'unico costo vero della via data URL è la dimensione - il carico utile sta circa il 33 percento più grande del file originale - e una grande immagine dentro un URL può mettere sotto tensione i limiti di stringa della pagina, un altro voto per gli URL di oggetto quando il file non deve mai lasciare il browser.

Decodificare i file che arrivano come testo

I file arrivano nel browser in due modi. Il modo moderno è byte grezzi: un fetch che leggi come ArrayBuffer, o un File di un selettore che leggi con file.arrayBuffer(). Se sei su quella strada, congratulazioni - il Base64 non c'è per niente nel quadro, e dovresti restare su quella strada, perché i byte non costano nulla da portare, mentre il Base64 costa un terzo in più di banda e memoria per il privilegio. L'altro modo è quando il canale è solo testuale: un'API JSON che restituisce {"attachment": "data:application/pdf;base64,JVBERi..."}, un allegato email, una stringa di configurazione, un valore in una colonna del database. Allora il Base64 è il protocollo, e il tuo lavoro è solo tirare fuori i byte:

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

Tre note su quel frammento di codice. Spezzare alla prima virgola basta per staccare l'intestazione del data URL (il tipo multimediale non può contenere virgole, quindi la prima è sempre il separatore). E se il valore è Base64 semplice senza prefisso data URL, salta pure lo spezzamento. Infine, quel await nudo è uno top-level, e i browser li permettono solo dentro i moduli, quindi il frammento ha bisogno di un tag <script type="module"> o di un wrapper asincrono attorno a quelle due righe. Le parti MIME delle email sono la stessa storia con qualche passo in più: il corpo dell'allegato è Base64 avvolto a 76 caratteri per riga, ma dato che atob() salta gli spazi bianchi, puoi passarle il testo avvolto esattamente come è arrivato nel messaggio grezzo - non serve togliere le righe. Quel singolo comportamento risparmia in silenzio un sacco di regex.

Verificare una negoziazione WebSocket

Uno degli usi più affascinanti della decodifica nel browser è controllare la negoziazione WebSocket stessa. L'RFC 6455 richiede al client di inviare un'intestazione Sec-WebSocket-Key (16 byte casuali, codificati in Base64) e al server di rispondere con Sec-WebSocket-Accept: l'hash SHA-1 della chiave concatenata a una GUID magica fissa, codificato in Base64. Se il valore non corrisponde, la negoziazione fallisce e la connessione non completa l'upgrade. Tutto il punto di questa cerimonia è che un server che parla solo HTTP non può completarla per errore - la GUID magica esiste per rendere il calcolo deliberatamente troppo complicato. E dato che il browser ha sia l'hashing sia la codifica, puoi calcolare tu la risposta attesa, il che rende il debug di proxy e passerelle una riga sola:

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="

Quell'ultima riga non è una coincidenza - è l'esempio esatto dell'RFC, riprodotto byte per byte. Quando la tua passerella risponde con qualsiasi altra cosa, ora sai precisamente quale lato dell'equazione sta mentendo.

Intestazioni HTTP e stringhe di query

Il Base64 è un favorito per le intestazioni HTTP perché le intestazioni devono essere ASCII, e il caso più famoso è l'autenticazione Basic: Authorization: Basic seguito dalla codifica Base64 di username:password. Leggere un'intestazione del genere (ad esempio mentre mostri cosa porta una richiesta) è uno spezzamento e una decodifica:

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

Il modello di espansione e ricomposizione gestisce il caso scomodo ma lecito di una password contenente due punti, perché il punto di spezzamento è sempre il primo, dopo il nome utente. Lo stesso modello vale ovunque un'intestazione contrabbanda un valore strutturato: Proxy-Authorization, alcune intestazioni specifiche del fornitore, e ogni tanto un cookie. Nelle stringhe di query e nei deep link, il Base64 appare quando un'app vuole condividere stato senza un server: un valore state di OAuth, un modulo di ricerca ripristinato, un marcatore "riprendi da dove avevi lasciato". Decodifica in modo difensivo - avvolta in try/catch, perché il valore ha attraversato un confine di rete e può essergli successo qualsiasi cosa - e tratta ciò che ottieni come input non fidato, punto.

E questo ci porta alla frase che dovrebbe essere inchiodata sopra ogni terminale: il Base64 non è crittografia. Non è nemmeno oscuramento in alcun senso reale, perché la "decrittazione" è una chiamata di funzione che ogni linguaggio sulla terra implementa. Se un valore deve restare segreto, codificarlo prima in Base64 lo rende meno sicuro, non di più - crea l'illusione della privacy e aggiunge esattamente un passo banale per chiunque voglia l'originale.

Stato nell'URL e nell'archiviazione

La stessa logica si estende a qualsiasi cosa che debba sopravvivere a un ricaricamento della pagina o a un link di condivisione. I soliti sospetti: valori di localStorage e sessionStorage che portano dati strutturati o binari, il frammento hash di un URL per lo stato di instradamento delle single-page app, e blocchi di configurazione incorporati nelle pagine dagli strumenti di build. La storia dell'archiviazione vale un esempio concreto, perché il lato lettura si abbina al lato scrittura che vorrai ricordarti:

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

Tre cose da tenere a mente. Prima, i budget: i browser danno a ogni origine circa 5 megabyte di localStorage, e la tua stringa Base64 salvata mangia circa il 33 percento in più rispetto ai dati originali, quindi un file da 3,5 megabyte diventa in silenzio 4,6 megabyte di archiviazione - e la stringa vive in memoria come UTF-16, il che raddoppia di nuovo l'impronta mentre la pagina è aperta. Secondo, la coerenza: codifica e decodifica con la stessa codifica di caratteri su entrambi i lati, altrimenti salverai byte perfettamente sani e leggerai un groviglio di caratteri incomprensibili. Terzo, i link di condivisione: se lo stato viaggia nell'URL, usa l'alfabeto sicuro per URL in modo che il valore sopravviva al copia-incolla, e tienilo breve, perché le lunghezze di URL oltre un paio di migliaia di caratteri cominciano a mettere in ansia i client più vecchi e gli strumenti di logging.

Quando i dati arrivano in pezzi

A volte il Base64 non arriva come una stringa sola: un confine di messaggio WebSocket lo taglia a metà, un flusso di eventi server-sent lo goccia, un upload a blocchi gliene porta pochi kilobyte alla volta. Non puoi chiamare atob() su un frammento, perché i gruppi Base64 sono unità di 3 byte espressi in blocchi di 4 caratteri, e un taglio in mezzo a un gruppo lascia un frammento parziale pendente. La correzione di scuola vecchia era accumulare i caratteri in una memoria tampone finché non si aveva un multiplo di quattro, e decodificare la memoria tampone a fette. L'API 2025 la rende pulita: Uint8Array.prototype.setFromBase64(string, options) scrive i byte decodificati in un array esistente e restituisce un oggetto con due numeri, read (quanti caratteri ha consumato) e written (quanti byte ha prodotto). Con lastChunkHandling: "stop-before-partial", decodifica solo i gruppi completi e lascia la coda parziale non letta, che è esattamente il comportamento che un decoder di flussi vuole:

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

Leggi lentamente il ciclo interno, perché è l'intero modello: alimenta con il resto portato avanti più il nuovo pezzo, lascia che il decoder consumi quanti gruppi completi ci stanno, ricorda quanto è rimasto tagliando via result.read caratteri, e quando non resta nulla di completo (result.read === 0) metti da parte il resto come nuovo avanzo e aspetta il pezzo successivo. Il Uint8Array(8) è solo una memoria tampone di lavoro - un gruppo di quattro caratteri produce al massimo tre byte, quindi otto è generoso. Alla fine, carry contiene ciò che il flusso non ha mai finito, che è o il tuo segnale di errore o il tuo controllo "la connessione è terminata pulita".

Quando non decodificare il Base64

Un riferimento utile ti insegna quando posare lo strumento. Se controlli entrambe le estremità del canale, prendi i byte grezzi: fetch con response.arrayBuffer() per i download, file.arrayBuffer() per i file del selettore, carichi utili ArrayBuffer nei WebSocket, e FormData multipart per gli upload. Nessuna di quelle cose tocca il Base64, e ottieni i dati a piena velocità, senza alcuna tassa dimensionale e senza alcuna impronta di stringa in memoria. Il Base64 si guadagna lo stipendio proprio quando il canale è solo testuale: corpi JSON, stringhe di query, email, archiviazione, API legacy, e qualsiasi cosa il cui contratto dice "ASCII o niente". Nel momento in cui basterebbe un byte, una stringa Base64 paga un sovrapprezzo del 33 percento per il privilegio di essere stampabile, e il sovrapprezzo è riscosso in banda, memoria e CPU - tre fatture che puoi evitare tutte.

Gli inciampi classici della decodifica

Dopo tutti i sentieri felici, ecco la lista dei modi in cui questo morde, più o meno nell'ordine in cui li incontrerai:

  • Trattare il risultato di atob() come testo. È una stringa binaria. Attraverso TextDecoder diventa testo; stampato direttamente, diventa un groviglio di caratteri incomprensibili. Questa singola confusione causa la maggior parte dei report "il Base64 non funziona".
  • Aspettarsi che l'Unicode funzioni da solo. I byte di "你好" si decodificano volentieri, ma restano byte finché un decoder non ti dice che sono UTF-8. Codifica e decodifica con la stessa codifica di caratteri su entrambi i lati.
  • Passare base64url ad atob(). Un solo - o _ lancia un'eccezione. Converti prima l'alfabeto, o usa fromBase64 con l'opzione giusta.
  • Credere che ogni stringa lunga sia Base64. Una stringa Base64 valida con riempimento ha una lunghezza che è un multiplo di quattro (il base64url senza riempimento può finire a 2 o 3) e usa al massimo un alfabeto. Una lunghezza di uno modulo quattro è un fallimento immediato - controllala prima di spendere un try/catch su di essa.
  • Fidarsi di un riempimento che non hai concordato. Alcuni sistemi rimuovono gli =, altri li tengono, e altri li aggiungono in mezzo a una stringa avvolta, dove non appartengono. Concorda con il mittente, poi decidi se essere tollerante (atob) o rigoroso (fromBase64).
  • Corruzione silenziosa da un decoder tollerante. Un TextDecoder di predefinito sostituisce i byte non validi con U+FFFD e non dice niente. Imposta fatal: true quando i dati contano.
  • Dare per scontato che il Base64 protegga qualcosa. Non lo fa. È un formato di serializzazione, a una chiamata di funzione dal testo semplice, e "lo codifichiamo in Base64 così gli utenti non possono leggerlo" è un atteggiamento di sicurezza, non un controllo.
  • Dimenticare la memoria. Una stringa binaria decodificata di un megabyte occupa due megabyte come stringa UTF-16, mentre un Uint8Array degli stessi dati occupa uno. Per carichi utili grandi, vai dritto a fromBase64.
  • Ridecodificare a ogni render. Decodificare pochi megabyte è veloce, ma non è gratis - e non è qualcosa da fare una volta per frame. Decodifica una volta, metti i byte in cache, renderizza dalla cache.

Note sulle prestazioni

La versione breve: i decoder nativi sono veloci, e la parte lenta del codice vecchio è di solito il JavaScript intorno a loro, non il Base64 in sé. Alle dimensioni che contano, il quadro è lo stesso: un carico utile da 10 megabyte si decodifica in millisecondi a una cifra con Uint8Array.fromBase64; da solo atob è alcune volte più lento, e il classico ciclo di seguito che mappa i caratteri in un array di byte richiede per lo stesso input circa venti volte di più di fromBase64, perché esegue circa tredici milioni di scritture di proprietà sul thread principale. Conseguenze pratiche: preferisci fromBase64 dove il tuo pubblico ce l'ha; tieni l'ausiliario atob dove non ce l'hanno; non costruire mai un array di byte concatenando stringhe in un ciclo; e se devi gestire un carico utile enorme, pensa di passare l'Uint8Array decodificato a un Web Worker - i byte passano senza copia, e il thread principale resta libero per tenere l'interfaccia a 60 frame al secondo. E ricorda la direzione del calcolo: la decodifica rimpicciolisce, quindi una memoria tampone decodificata occupa sempre meno memoria della stringa da cui proviene. Non puoi mai finire la memoria decodificando; puoi solo finire la memoria tenendo in giro più a lungo del necessario sia la stringa sia i byte.

Una breve storia della decodifica nei browser

Il Base64 è più vecchio della maggior parte del web moderno, ma i decoder dei browser hanno una storia che vale la pena conoscere, perché spiega perché l'ecosistema è pieno di relitti. atob e la sua sorella btoa precedono la specifica che ora le copre: lo standard HTML del WHATWG le ha definite solo a febbraio 2011, quando il loro comportamento pluriennale nei browser è stato ricavato a ritroso nello standard. I motori le avevano comunque distribuite presto: Firefox dalla versione 1 nel 2004, Safari 3, Chrome 4. Internet Explorer le ha saltate del tutto fino a IE 10 nel 2012, ed è per questo che il JavaScript pre-2012 è un museo di Base64 fatto a mano - tabelle di ricerca, ginnastica di String.fromCharCode, e l'infame incantesimo unescape(encodeURIComponent()) per l'Unicode, una coppia di funzioni deprecate nel linguaggio e sopravvissute nei browser per un decennio per pura inerzia. Poi è arrivato il livello delle codifiche di caratteri: TextEncoder e TextDecoder dallo standard Encoding sono arrivati tra il 2013 e il 2017 (Firefox 18, Chrome 38, Safari 10.1, e mai in nessuna IE), dando finalmente alla piattaforma un modo di principio per trasformare i byte in parole. Node.js, che non ha mai avuto atob o btoa come globali fino alla versione 16 nel 2021, ha passato la sua vita precedente con Buffer e una coppia di piccoli shim npm. E poi il cerchio si è chiuso: Firefox 133 (novembre 2024) e Safari 18.2 (dicembre 2024) hanno distribuito per primi Uint8Array.fromBase64, toBase64 e compagnia, e la seconda metà del 2025 ha completato il set quando sono arrivati Chrome 140 (settembre) e Node 25 (metà ottobre) e il programma Baseline li ha marchiati Newly available, la prima volta che il linguaggio in sé - non la piattaforma web - ha ottenuto il Base64 integrato. Un formato vecchio di decenni è appena diventato una funzionalità della libreria standard del linguaggio, e il prossimo decennio di codice può smettere di copiare funzioni ausiliarie di qua e di là.

Fatti curiosi

  • Il test "questo è davvero Base64?" più veloce in circolazione è string.length % 4 === 0. Ogni stringa Base64 valida e con riempimento lo passa; qualsiasi altra cosa è una sconosciuta.
  • atob('') restituisce ''. La stringa vuota è l'unico input senza byte, e fa l'andata e il ritorno senza problemi attraverso tutta la linea di produzione - nessun caso speciale necessario, mai.
  • La GUID magica del WebSocket, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, è un valore fisso incorporato nell'RFC, scelto in modo che un server HTTP semplice non potesse mai completare per errore la negoziazione. È la costante più famosa dell'ingegneria dei protocolli che nessuno genera mai.
  • Chrome e Firefox lanciano la stessa eccezione per lo stesso fallimento ma con messaggi diversi. Cattura su error.name, non sulla stringa del messaggio, o la tua gestione degli errori avrà un accento di browser.
  • Una stringa binaria da un megabyte pesa due megabyte in memoria, perché le stringhe JavaScript sono UTF-16: ogni byte decodificato viaggia accompagnato da un byte di spazio inutilizzato. L'Uint8Array non ha una tassa del genere.
  • "Data URI" è un nome in pensione. Il WHATWG l'ha rinominato "data URL" come parte della grande armonizzazione da URI a URL, ed è per questo che incontrerai entrambe le grafie in specifiche, post e nomi di pacchetto.
  • L'RFC 4648 distribuisce una tabella di vettori di test - "f", "fo", "foo", "foob", "fooba", "foobar" e compagnia, ognuno con la sua codifica nota - a cui gli autori di decoder si confrontano da vent'anni. Se il tuo decoder passa quelle righe, è quasi certamente corretto.
  • La stringa Base64 più prodotta nella storia del calcolo è quasi certamente aGVsbG8=, la codifica di "hello". Ogni tutorial di "per iniziare", suite di test e risposta su Stack Overflow sul pianeta contribuisce il proprio voto.

Per concludere

Quindi tutto il mestiere della decodifica nel browser sta in una pagina: atob() per la decodifica rapida, tollerante, universale; TextDecoder per trasformare i byte nelle parole che vuoi davvero, con fatal: true quando i dati contano; e Uint8Array.fromBase64 per la via moderna, rigorosa, veloce che salta la stringa del tutto. In mezzo, le varianti hanno nomi e regole: base64url per tutto ciò che viaggia in un URL, riempimento che può esserci o non esserci, spazi bianchi che il vecchio decoder mangia in silenzio. E sotto tutto questo, due atteggiamenti: i byte non sono il testo, e il testo non è il segreto. Decodifica di proposito, verifica prima di fidarti, e quando il canale lo permette, salta il Base64 e prendi i byte.

L'altra metà del viaggio - prendere i tuoi byte e testo e trasformarli nella stringa stampabile da cui è partito tutto questo - è coperta in dettaglio nella guida gemella alla codifica Base64 in JavaScript, collegata qui sotto.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Codifica Base64 in JavaScript/Browser: una guida completa