Vous avez à traiter le format Base64 ? Alors ce site est parfait pour vous ! Utilisez notre outil en ligne super pratique pour encoder ou décoder vos données.

Décodage Base64 en JavaScript/Browser : un guide complet

Il se pointe sous une douzaine de déguisements différents : un JWT glissé dans un en-tête Authorization, un blob image/png au milieu d'une réponse JSON, une valeur Sec-WebSocket-Accept dans un log de handshake, une pièce jointe d'e-mail enveloppée en MIME, une valeur que votre backend a poliment rangée dans une chaîne de requête. La chaîne elle-même a toujours le même air : une longue suite de lettres et de chiffres, un + ou un / de temps en temps, et peut-être un = ou deux à la fin. Si la page d'accueil de ce site vous a montré ce qu'est le Base64 - quatre caractères imprimables qui tiennent la place de trois octets, avec un remplissage = pour finir le dernier groupe - alors cet article parle de la partie que vous faites vraiment dans le code : transformer ces caractères en octets, puis ces octets en sens, sans rien utiliser d'autre que ce que le navigateur embarque déjà.

Deux règles de base rapides avant de commencer. D'abord, le décodage est la direction qui rétrécit : pour chaque groupe de quatre caractères lu, trois octets sortent, donc la sortie prend toujours moins de mémoire que l'entrée. Ensuite, une chaîne Base64 décodée n'est pas automatiquement du texte. Ce sont des octets, et ces octets peuvent s'avérer être du UTF-8, du Windows-1252, un en-tête PNG ou une signature cryptographique. Le bug le plus fréquent du code Base64 est d'oublier lequel de ces rôles vous tenez dans les mains, et les sections ci-dessous sont organisées autour de cette question.

Les trois couches du décodage

Les navigateurs modernes vous offrent trois couches natives, et la bonne nouvelle, c'est qu'aucun package n'est jamais nécessaire. Chacune répond à une question légèrement différente, et choisir la bonne vous épargne bien des extraits de Stack Overflow recopiés en vrac :

Couche Ce qu'elle mange Ce qu'elle vous rend Personnalité Disponibilité
atob() chaîne Base64 standard une « chaîne binaire » (un octet par caractère) très tolérante : saute les espaces ASCII, accepte le remplissage manquant tous les navigateurs depuis les années 2000, IE 10+, Node 16+
TextDecoder des octets (Uint8Array) du texte JavaScript lisible configurable : une étiquette pour le charset, un flag fatal pour la rigueur Firefox 18, Chrome 38, Safari 10.1 et au-delà (jamais IE)
Uint8Array.fromBase64() chaîne Base64 plus des options un vrai Uint8Array stricte avec des molettes : alphabet et gestion du dernier bloc Baseline 2025 : Chrome 140, Firefox 133, Safari 18.2, Node 25

Toute la forme de l'article découle de ce tableau. atob() est le cheval de trait que vous croiserez partout, y compris dans le vieux code. TextDecoder est le pont entre les octets et les mots. Et Uint8Array.fromBase64() est la mise à niveau de 2025 qui saute purement et simplement l'étape intermédiaire quand vous ne vouliez que des octets dès le départ.

atob : rapide, tolérant et très ancien

Tout le contrat tient en une ligne : atob(encodedData). Il prend une chaîne encodée en Base64 et renvoie une « chaîne binaire » : une chaîne JavaScript ordinaire où chaque caractère porte exactement un octet décodé, un point de code de 0 à 255. Ce type de retour compte, parce qu'il n'est pas la même chose que du texte lisible (on y reviendra plus bas). La fonction elle-même est rapide comme l'éclair, et elle est là depuis très longtemps : Chrome 4, Firefox 1, Safari 3, et - c'est le détail que tout le monde se rappelle - Internet Explorer seulement à partir de la version 10, ce qui explique pourquoi le code écrit avant 2012 regorge de tables Base64 faites à la main.

Ce qui rend atob() agréable, c'est tout ce qu'il pardonne avant de se donner pour vaincu. La norme HTML WHATWG dit d'ignorer tous les espaces ASCII - espace, tabulation, retour à la ligne, saut de page, retour chariot - avant de décoder, donc une chaîne enveloppée en MIME avec des retours à la ligne tous les 76 caractères se décode sans aucun nettoyage de votre côté. Le remplissage manquant est pardonné, lui aussi. Mais au moment où il voit un caractère en dehors de l'alphabet, ou une longueur qui ne pourrait jamais être valide, il lève une DOMException nommée InvalidCharacterError. Pas de n'importe quoi en silence, pas de résultats partiels.

Voici le rapport des dégâts, ligne par ligne :

Entrée Résultat
"SGVsbG8sIFdvcmxkIQ==" "Hello, World!", le cas du manuel
"aGVsbG8" (sans remplissage) "hello", un = manquant est pardonné
"SGVs\nbG8s\nIFdvcmxkIQ==" (lignes enveloppées) "Hello, World!", les espaces ASCII sont sautés d'abord
"" (chaîne vide) "", l'entrée vide est valide et fait l'aller-retour
"A" (un caractère restant) lève InvalidCharacterError, un caractère ne peut rien encoder
"Zm9vYmFy!" (un ! errant) lève InvalidCharacterError, en dehors de l'alphabet
"ZGFua29nYWk-" (caractère URL-safe mélangé) lève InvalidCharacterError, les deux alphabets ne doivent pas être mélangés
"Zm9v====" (trop de remplissage) lève InvalidCharacterError, au plus deux = à la fin

Une note pratique : le message d'erreur lui-même diffère d'un moteur à l'autre (Firefox dit « String contains an invalid character », Chrome dit que la chaîne « contains characters outside of the Latin1 range » pour une entrée non Latin1, ou « is not correctly encoded » pour du Base64 invalide), donc attrapez sur le nom de l'exception, pas sur le texte du message.

Des octets bruts au vrai texte

Ce type de retour « chaîne binaire » mérite un point d'arrêt, parce qu'il est à l'origine de la plupart des confusions du décodage. Les chaînes JavaScript sont en UTF-16, donc atob() vous rend une chaîne dont les caractères sont des valeurs d'octets, pas des glyphes lisibles. Si votre payload était l'encodage UTF-8 du texte « hello 你好 », afficher le résultat directement vous donne du mojibake. La solution est un décodage en deux temps : Base64 en octets, puis octets en texte.

D'abord, l'étape Base64 vers octets. Ce petit utilitaire est la recette classique et il vaut la peine de le garder dans sa poche, parce qu'il est la pièce maîtresse de la plupart des exemples de cet article :

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

Puis l'étape octets vers texte, avec TextDecoder. Pour le UTF-8 (le choix par défaut, et le bon choix pour le JSON, les payloads JWT et la plupart des données web), l'appel tient en une ligne :

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

Pourquoi deux étapes ? Parce que atob() n'a aucune idée du charset dans lequel les octets ont été produits. C'est un convertisseur pur de bits. TextDecoder est le composant qui interprète des octets comme un charset, et il accepte une étiquette pour le travail : utf-8, windows-1252, iso-8859-1, utf-16le, plus environ 220 autres étiquettes. Les données sorties d'une application des années 1990 sont d'habitude du Windows-1252, et un seul argument du constructeur suffit :

const decoder = new TextDecoder('windows-1252');
const text = decoder.decode(bytes); // mêmes octets, interprétation différente

Le constructeur TextDecoder accepte aussi un flag fatal, et il vaut la peine de le mettre à true chaque fois que le texte décodé alimente quelque chose d'important. Par défaut le décodeur est indulgent : les suites d'octets invalides sont remplacées en silence par le caractère de remplacement Unicode, U+FFFD, et on ne vous dit rien. Avec fatal: true, le même dommage lève une TypeError au lieu de se cacher :

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

C'est l'un de ces interrupteurs qui paraissent anodins dans la doc et ressemblent à un incident de données en production. Si votre entrée vient d'un utilisateur ou du réseau, décodez en mode strict et traitez l'erreur exprès.

Les entrées URL-safe exigent un détour

Une variante du Base64 mérite sa propre section, parce qu'elle apparaît constamment dans la nature et que atob() refuse de la lire. C'est l'alphabet sûr pour les URL et les noms de fichiers de la section 5 du RFC 4648, habituellement appelé base64url : les mêmes 64 caractères, sauf que + et / sont remplacés par - et _, et le remplissage = est souvent retiré, puisque la longueur des données est connue implicitement. L'échange existe pour une raison concrète : dans une URL, + signifie une espace et / commence un segment de chemin, donc l'alphabet standard devrait être encodé en pourcentage caractère par caractère. Le Base64url voyage proprement dans les chaînes de requête, les segments de chemin, les fragments et les noms de fichiers.

Le piège, c'est que les deux alphabets ne sont pas interchangeables, et atob() ne parle que le standard. Passez-lui un - ou un _ et vous obtenez InvalidCharacterError. Vous avez deux options propres.

Option un, qui fonctionne partout : convertir l'alphabet et restaurer le remplissage avant d'appeler 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'expression (4 - (s.length % 4)) % 4 est toute l'astuce : elle calcule combien de caractères = aurait besoin une chaîne bien remplie de cette longueur, de zéro jusqu'à deux.

Option deux, dans les navigateurs de 2025 et plus : le nouveau décodeur natif prend l'alphabet comme option, donc aucune chirurgie sur la chaîne :

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

Deux règles vous gardent à l'abri des ennuis. Ne mélangez jamais d'alphabets dans une seule valeur - un décodeur qui voit à la fois un + et un - n'a aucun moyen de savoir quelle famille il lit, et le comportement conforme à la spécification est d'échouer. Et mettez-vous d'accord avec l'autre bout du fil sur la présence ou non du remplissage : le retirer est légal pour le base64url, donc un récepteur doit être prêt pour les deux formes. atob() l'est déjà ; les options natives ci-dessous vous donnent une molette pour ça.

L'astuce de 2025 : Uint8Array.fromBase64

Si vous regardez en arrière l'utilitaire base64ToBytes, vous remarquerez qu'il fait deux choses : décoder le Base64, puis copier les caractères dans un tableau d'octets un par un, en JavaScript. Cette boucle de copie est la partie lente et évitable, et c'est exactement ce que supprime la nouvelle méthode ECMAScript. Uint8Array.fromBase64(string, options) va directement de la chaîne encodée au tableau d'octets, et elle est livrée dans Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 et Deno 2.5 - la première fonctionnalité de plateforme JavaScript de ce type à atterrir, marquée Baseline Newly available dans le programme Baseline des éditeurs de navigateurs.

L'objet d'options a deux molettes. La première est alphabet : "base64" (la valeur par défaut) ou "base64url". La deuxième est lastChunkHandling, qui contrôle ce qui arrive au dernier groupe partiel de caractères :

Mode Règle pour le dernier bloc
"loose" (par défaut) deux ou trois caractères, ou quatre avec remplissage ; les bits de dépassement restants sont ignorés
"strict" exactement quatre caractères (remplissage seulement là où la longueur l'exige), et les bits de dépassement doivent tous être à zéro
"stop-before-partial" seuls les groupes complets de quatre caractères sont décodés ; une queue partielle reste non lue

Comme atob(), la méthode ignore les espaces ASCII dans l'entrée, donc les lignes enveloppées passent sans souci. À la différence de atob(), elle a une opinion sur tout le reste : un caractère en dehors de l'alphabet choisi, ou un dernier bloc qui viole le mode choisi, lève une SyntaxError ; passer quelque chose qui n'est pas une chaîne lève une TypeError. Voici le mode strict à l'œuvre, rejetant un bloc dont le remplissage est manquant :

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

Les performances sont l'autre raison de la préférer. Sur un Firefox récent sur la machine de l'auteur, décoder un payload de 10 mégaoctets prend quelques millisecondes à un chiffre avec fromBase64, alors que le classique atob plus le mappage caractère par caractère vers des octets prend environ vingt fois plus longtemps, parce que la partie lente est la boucle au niveau JavaScript, pas l'arithmétique du Base64. Si vos données sont des octets, sautez la chaîne entièrement.

Pour les navigateurs plus anciens, la situation est simple : gardez l'utilitaire base64ToBytes ci-dessus, ou ajoutez un petit polyfill (core-js et le package es-arraybuffer-base64 du projet es-shims en embarquent tous les deux un pour fromBase64) si vous voulez écrire du code à la nouvelle manière partout. L'API est stable - elle est dans la spécification ECMAScript maintenant - donc ce que vous écrivez dessus ne sera pas déprécié.

Lire un JWT

La « chaîne mystérieuse » la plus courante dans les logs d'application est un JSON Web Token : trois segments séparés par des points, header.payload.signature, où les deux premiers sont des objets JSON encodés en base64url. En décoder un tient en cinq lignes, et c'est un échauffement parfait pour tout ce qui précède :

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"

Maintenant, la partie que les débutants sautent et que les systèmes de production découvrent à la dure : le payload n'est pas validé par le fait qu'il soit décodable. N'importe qui peut écrire un JWT avec n'importe quel payload ; c'est le segment de signature qui l'attache à un secret. Vérifier un token HS256 dans le navigateur utilise l'API Web Crypto, qui a besoin de la signature en octets - une raison de plus pour laquelle l'utilitaire segment vers octets gagne sa place :

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 uniquement si la signature correspond au secret

Trois pièges méritent d'être nommés. D'abord, vérifiez l'en-tête avant de vérifier : un token qui affirme alg: "none" vous demande de faire confiance au payload sans signature, et du code naïf a été trompé pour faire exactement cela. Ensuite, honorez les claims temporels - exp, nbf, iat - après la vérification, pas avant. Troisièmement, l'attaque classique de confusion de clés : un serveur configuré pour RS256 mais qui accepte aussi le HS256 laisse un attaquant signer des tokens avec la clé publique (publique exprès) utilisée comme secret HMAC. En bref : décodez librement, ne faites confiance à rien, vérifiez tout.

Ouvrir des data URLs

Une data URL embarque un fichier entier dans une URL : data:, un type média optionnel, un flag ;base64 optionnel, une virgule, puis le payload. Les payloads texte sont encodés en pourcentage, les payloads binaires sont en Base64, et le navigateur les affiche sans aucune requête HTTP - pas de fetch, pas d'aller-retour serveur, rien à mettre en cache. Le navigateur traite chaque data URL comme une origine unique et opaque, ce qui explique aussi pourquoi elles sont le vecteur favori des contenus fourbes : un document data:text/html ouvert dans une iframe exécute ses scripts, et une Content-Security-Policy restrictive peut bloquer les data URLs entièrement. Gardez votre CSP en tête si vous commencez à en remettre dans du balisage contrôlé par l'utilisateur.

En décoder une revient surtout à de la chirurgie sur la chaîne, puis c'est le même pipeline d'octets que d'habitude :

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

Le morceau meta vous dit le type média (ici image/png, avec le marqueur ;base64 qui confirme que le payload est en Base64). Une fois le payload devenu un Blob, tout le reste s'applique : une object URL pour un <img>, un lien de téléchargement, ou un POST vers un serveur. Le seul vrai coût de la voie data URL est la taille - le payload pèse environ 33 pour cent de plus que le fichier d'origine - et une grande image dans une URL peut mettre à mal les limites de chaîne de la page, ce qui est un argument de plus pour les object URLs quand le fichier n'a jamais besoin de quitter le navigateur.

Décoder des fichiers qui arrivent en texte

Les fichiers atteignent le navigateur de deux façons. La voie moderne, c'est les octets bruts : un fetch que vous lisez en ArrayBuffer, ou un File venu d'un sélecteur que vous lisez avec file.arrayBuffer(). Si vous êtes sur cette voie, félicitations - il n'y a aucune trace de Base64 dans l'histoire, et vous devriez y rester, parce que les octets ne coûtent rien à porter tandis que le Base64 coûte un tiers supplémentaire de bande passante et de mémoire pour le privilège. L'autre voie, c'est quand le canal n'accepte que du texte : une API JSON qui renvoie {"attachment": "data:application/pdf;base64,JVBERi..."}, une pièce jointe d'e-mail, une chaîne de configuration, une valeur dans une colonne de base de données. Alors le Base64 est le protocole, et votre travail est simplement d'en extraire les octets :

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

Trois notes sur cet extrait. Couper à la première virgule suffit à peler l'en-tête data URL (le type média ne peut contenir aucune virgule, donc la première est toujours le séparateur). Et si la valeur est du Base64 pur sans préfixe data URL, sautez simplement le découpage. Enfin, ce await nu est un await de niveau supérieur, et les navigateurs n'en autorisent que dans les modules, donc l'extrait a besoin d'une balise <script type="module"> ou d'un wrapper async autour de ces deux lignes. Les parties MIME des e-mails sont la même histoire avec des étapes en plus : le corps de la pièce jointe est du Base64 enveloppé à 76 caractères par ligne, mais comme atob() saute les espaces, vous pouvez lui remettre le texte enveloppé exactement tel qu'il est arrivé dans le message brut - aucun nettoyage nécessaire. Ce seul comportement économise en silence beaucoup de regex.

Vérifier un handshake WebSocket

L'un des usages les plus charmants du décodage dans le navigateur, c'est de contrôler le handshake WebSocket lui-même. Le RFC 6455 exige que le client envoie un en-tête Sec-WebSocket-Key (16 octets aléatoires, encodés en Base64), et que le serveur réponde par Sec-WebSocket-Accept : le hash SHA-1 de la clé concaténée avec un GUID magique fixe, le tout encodé en Base64. Si la valeur ne correspond pas, le handshake échoue et la connexion n'est pas mise à niveau. Tout l'objet de cette cérémonie, c'est qu'un serveur qui ne parle que HTTP ne peut pas l'accomplir par accident - le GUID magique existe pour que le calcul paraisse volontairement surcompliqué. Et comme le navigateur a à la fois le hash et l'encodage, vous pouvez calculer la réponse attendue vous-même, ce qui rend le débogage des proxies et des passerelles en une seule ligne :

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

Cette dernière ligne n'est pas un hasard - c'est l'exemple exact du RFC, reproduit octet par octet. Quand votre passerelle répond autre chose, vous savez maintenant précisément quel côté de l'équation ment.

En-têtes HTTP et chaînes de requête

Le Base64 est un favori des en-têtes HTTP parce que les en-têtes doivent être en ASCII, et le cas le plus célèbre est l'authentification Basic : Authorization: Basic suivi de l'encodage Base64 de username:password. Lire un tel en-tête (disons en affichant ce qu'une requête transporte) tient en un découpage et un décodage :

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

Le motif spread-et-rejoin gère le cas embarrassant mais légal d'un mot de passe contenant deux-points, parce que le point de découpage est toujours le premier, juste après le nom d'utilisateur. Le même motif s'applique partout où un en-tête faufile une valeur structurée : Proxy-Authorization, certains en-têtes spécifiques à un éditeur, et l'occasionnel cookie. Dans les chaînes de requête et les deep links, le Base64 apparaît quand une application veut partager un état sans serveur : une valeur OAuth state, un formulaire de recherche restauré, un marqueur « reprendre là où j'en étais ». Décodez défensivement - enveloppez en try/catch, parce que la valeur a franchi une frontière réseau et n'importe quoi lui est arrivé - et traitez ce que vous obtenez comme une entrée non fiable, point final.

Ce qui nous amène à la phrase qu'il faudrait accrocher au-dessus de chaque terminal : le Base64 n'est pas un chiffrement. Ce n'est même pas de l'obfuscation au sens réel du terme, parce que le « décryptage » est un seul appel de fonction que chaque langage de la planète implémente. Si une valeur doit rester secrète, l'encoder en Base64 d'abord la rend moins sûre, pas plus - il crée l'illusion de l'intimité et ajoute exactement une étape triviale pour quiconque veut l'original.

L'état dans l'URL et dans le stockage

La même logique s'étend à tout ce qui doit survivre à un rechargement de page ou à un lien de partage. Les suspects habituels : les valeurs de localStorage et de sessionStorage qui portent des données structurées ou binaires, le fragment hash d'une URL pour l'état de routage des single-page-apps, et les blobs de configuration embarqués dans les pages par les outils de build. L'histoire du stockage mérite un exemple concret, parce que le côté lecture va de pair avec le côté écriture que vous voudrez retenir :

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

Trois choses à garder en tête. D'abord, les budgets : les navigateurs donnent environ 5 mégaoctets de localStorage par origine, et votre chaîne Base64 stockée mange environ 33 pour cent de plus que les données d'origine, donc un fichier de 3,5 mégaoctets devient discrètement 4,6 mégaoctets de stockage - et la chaîne vit en mémoire en UTF-16, ce qui double à nouveau l'empreinte pendant que la page est ouverte. Ensuite, la cohérence : encodez et décodez avec le même charset des deux côtés, sinon vous stockerez de parfaits octets et lirez du mojibake. Enfin, les liens de partage : si l'état voyage dans l'URL, utilisez l'alphabet URL-safe pour que la valeur survive au copier-coller, et gardez-la courte, parce que les longueurs d'URL au-dessus de quelques milliers de caractères commencent à inquiéter les vieux clients et les outils de logs.

Quand les données arrivent en morceaux

Parfois, le Base64 n'arrive pas en une seule chaîne : une limite de message WebSocket le coupe en deux, un flux d'événements server-sent events le dépose goutte à goutte, un upload par morceaux l'alimente quelques kilo-octets à la fois. On ne peut pas appeler atob() sur un fragment. Les groupes Base64 sont des unités de 3 octets exprimées en blocs de 4 caractères, et une coupure au milieu d'un groupe laisse un fragment partiel en suspens. La solution de l'école ancienne était de mettre les caractères en buffer jusqu'à avoir un multiple de 4, puis de décoder le buffer en tranches. L'API de 2025 rend ça propre : Uint8Array.prototype.setFromBase64(string, options) écrit les octets décodés dans un tableau existant et renvoie un objet avec deux nombres, read (combien de caractères il a consommés) et written (combien d'octets il a produits). Avec lastChunkHandling: "stop-before-partial", il ne décode que les groupes complets et laisse la queue partielle non lue. C'est exactement le comportement qu'un décodeur par flux veut :

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

Lisez la boucle intérieure lentement, parce que c'est tout le motif : alimentez avec le reste reporté plus le nouveau morceau, laissez le décodeur consommer autant de groupes complets que possible, souvenez-vous de ce qui reste en retranchant result.read caractères, et quand il ne reste plus rien de complet (result.read === 0), rangez le reste comme nouveau carry et attendez le morceau suivant. Le Uint8Array(8) n'est qu'un buffer de travail - un groupe de 4 caractères produit au plus 3 octets, donc 8, c'est large. À la fin, carry tient tout ce que le flux n'a jamais fini, ce qui est soit votre signal d'erreur, soit votre vérification « connexion terminée proprement ».

Quand ne pas décoder de Base64

Une référence utile vous apprend quand poser l'outil. Si vous contrôlez les deux bouts du canal, prenez des octets bruts à la place : fetch avec response.arrayBuffer() pour les téléchargements, file.arrayBuffer() pour les fichiers du sélecteur, des payloads ArrayBuffer dans les WebSockets, et du FormData multipart pour les uploads. Rien de tout cela ne touche au Base64, et vous obtenez les données à pleine vitesse, sans aucune taxe de taille et sans aucune empreinte de chaîne en mémoire. Le Base64 mérite son poids précisément quand le canal n'accepte que du texte : corps JSON, chaînes de requête, e-mails, stockage, API héritées, et tout ce dont le contrat dit « ASCII ou rien ». Au moment où un octet suffirait, une chaîne Base64 paie un supplément de 33 pour cent pour le privilège d'être imprimable, et le supplément est collecté en bande passante, mémoire et CPU - trois factures que vous pouvez toutes éviter.

Les pièges courants du décodage

Après tous les chemins heureux, voici la liste des façons dont ça mord, grosso modo dans l'ordre où vous les croiserez :

  • Traiter le résultat de atob() comme du texte. C'est une chaîne binaire. En passant par TextDecoder, elle devient du texte ; affichée directement, elle devient du mojibake. Cette seule confusion cause la plupart des signalements « le Base64 ne marche pas ».
  • Attendre que l'Unicode fonctionne tout seul. Les octets de « 你好 » se décodent parfaitement, mais ce sont toujours des octets jusqu'à ce qu'un décodeur vous dise que c'est du UTF-8. Encodez et décodez dans le même charset des deux côtés.
  • Alimenter atob() avec du base64url. Un seul - ou _ lève une exception. Convertissez l'alphabet d'abord, ou utilisez fromBase64 avec la bonne option.
  • Croire que toute chaîne longue est du Base64. Une chaîne Base64 valide avec remplissage a une longueur multiple de quatre (le base64url sans remplissage peut s'arrêter à 2 ou 3) et n'utilise qu'un seul alphabet au maximum. Une longueur égale à un modulo quatre est un échec immédiat - vérifiez-le avant de dépenser un try/catch dessus.
  • Faire confiance à un remplissage sur lequel vous ne vous êtes pas mis d'accord. Certains systèmes retirent les =, d'autres les gardent, et certains les ajoutent au milieu d'une chaîne enveloppée, où ils n'ont rien à faire. Mettez-vous d'accord avec l'expéditeur, puis décidez d'être indulgent (atob) ou strict (fromBase64).
  • Corruption silencieuse d'un décodeur indulgent. Un TextDecoder par défaut remplace les octets invalides par U+FFFD et ne dit rien. Mettez fatal: true quand les données comptent.
  • Supposer que le Base64 protège quelque chose. Ce n'est pas le cas. C'est un format de sérialisation, à un seul appel de fonction du texte brut, et « on le met en Base64 pour que les utilisateurs ne puissent pas le lire » est une posture de sécurité, pas un contrôle.
  • Oublier la mémoire. Une chaîne binaire décodée d'un mégaoctet occupe deux mégaoctets en chaîne UTF-16, tandis qu'un Uint8Array de la même donnée en occupe un. Pour les gros payloads, allez directement à fromBase64.
  • Ré-décoder à chaque rendu. Décoder quelques mégaoctets est rapide, mais pas gratuit - et ce n'est pas quelque chose à faire une fois par image. Décodez une fois, mettez les octets en cache, et rendez depuis le cache.

Notes de performance

La version courte : les décodeurs natifs sont rapides, et la partie lente du vieux code est d'habitude le JavaScript autour d'eux, pas le Base64 lui-même. Aux tailles qui comptent, le tableau est le même : un payload de 10 mégaoctets se décode en quelques millisecondes à un chiffre avec Uint8Array.fromBase64 ; atob seul est quelques fois plus lent, et la boucle de suite classique qui mappe les caractères vers un tableau d'octets prend environ vingt fois plus longtemps que fromBase64 pour la même entrée, parce qu'elle exécute environ treize millions d'écritures de propriétés sur le thread principal. Conséquences pratiques : préférez fromBase64 où votre audience l'a ; gardez l'utilitaire atob où elle ne l'a pas ; ne construisez jamais un tableau d'octets en concaténant des chaînes dans une boucle ; et si vous devez traiter un payload énorme, pensez à remettre le Uint8Array décodé à un Web Worker - les octets transitent sans copie, et le thread principal reste libre pour tenir l'interface à 60 images par seconde. Et rappelez-vous la direction de l'arithmétique : le décodage rétrécit, donc un buffer décodé prend toujours moins de mémoire que la chaîne dont il vient. Vous ne pouvez jamais manquer de mémoire en décodant ; vous ne pouvez manquer de mémoire qu'en gardant la chaîne et les octets plus longtemps que nécessaire.

Une brève histoire du décodage dans les navigateurs

Le Base64 est plus vieux que la plupart du web moderne, mais les décodeurs des navigateurs ont une histoire qui vaut la peine d'être connue, parce qu'elle explique pourquoi l'écosystème regorge de reliques. atob et sa sœur btoa sont antérieurs à la spécification qui les couvre maintenant : la norme HTML WHATWG ne les a définis qu'en février 2011, quand leur comportement navigateur bien installé a été rétro-ingéniéré dans la norme. Les moteurs les avaient de toute façon livrés tôt : Firefox depuis la version 1 en 2004, Safari 3, Chrome 4. Internet Explorer les a entièrement ignorés jusqu'à IE 10 en 2012, ce qui explique pourquoi le JavaScript d'avant 2012 est un musée de Base64 fait à la main - tables de recherche, gymnastiques de String.fromCharCode, et l'invocation infâme unescape(encodeURIComponent()) pour l'Unicode, une paire de fonctions dépréciées dans le langage et survivues dans les navigateurs pendant une décennie par pure inertie. Puis est venue la couche charset : TextEncoder et TextDecoder de la norme Encoding sont arrivés entre 2013 et 2017 (Firefox 18, Chrome 38, Safari 10.1, et jamais dans aucune version d'IE), offrant enfin à la plateforme une façon principielle de transformer des octets en mots. Node.js, qui n'a jamais eu atob ou btoa comme globales avant la version 16 en 2021, a passé sa jeunesse avec Buffer et une paire de petits shims npm. Et puis la boucle s'est refermée : Firefox 133 (novembre 2024) et Safari 18.2 (décembre 2024) ont d'abord livré Uint8Array.fromBase64, toBase64 et leurs amis, et la seconde moitié de 2025 a terminé le set quand Chrome 140 (septembre) et Node 25 (mi-octobre) sont arrivés et que le programme Baseline les a marqués Newly available, la première fois que le langage lui-même - pas la plateforme web - a eu le Base64 intégré. Un format vieux de plusieurs décennies vient de devenir une fonctionnalité de bibliothèque standard du langage, et la prochaine décennie de code peut cesser de recopier des utilitaires partout.

Faits amusants

  • Le test « est-ce vraiment du Base64 ? » le plus rapide qui soit est string.length % 4 === 0. Toute chaîne Base64 valide avec remplissage le passe ; tout le reste est un étranger.
  • atob('') renvoie ''. La chaîne vide est la seule entrée sans octets, et elle fait l'aller-retour proprement dans tout le pipeline - aucun cas particulier nécessaire, jamais.
  • Le GUID magique des WebSockets, 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, est une valeur fixe gravée dans le RFC, choisie pour qu'un serveur HTTP pur ne puisse jamais accomplir le handshake par accident. C'est la constante la plus célèbre de l'ingénierie de protocoles que personne ne génère jamais.
  • Chrome et Firefox lèvent la même exception pour la même défaillance, mais avec des messages différents. Attrapez sur error.name, pas sur la chaîne du message, sinon votre gestion des erreurs aura un accent navigateur.
  • Une chaîne binaire d'un mégaoctet pèse deux mégaoctets en mémoire, parce que les chaînes JavaScript sont en UTF-16 : chaque octet décodé voyage avec un octet de marge inutilisée. Le Uint8Array n'a pas cette taxe.
  • « Data URI » est un nom à la retraite. Le WHATWG l'a renommé en « data URL » dans le cadre de la grande harmonisation URI-vers-URL, c'est pourquoi vous croiserez les deux orthographes dans les spécifications, les articles et les noms de packages.
  • Le RFC 4648 embarque un tableau de vecteurs de test - « f », « fo », « foo », « foob », « fooba », « foobar » et leurs amis, chacun avec son encodage connu - que les auteurs de décodeurs vérifient depuis vingt ans. Si votre décodeur passe ces lignes, il est presque certainement correct.
  • La chaîne Base64 la plus produite de l'histoire de l'informatique est presque sûrement aGVsbG8=, l'encodage de « hello ». Chaque tutoriel « pour débuter », chaque suite de tests et chaque réponse Stack Overflow sur la planète ajoute son vote.

En résumé

Donc tout l'art du décodage dans le navigateur tient sur une page : atob() pour le décodage rapide, tolérant et universel ; TextDecoder pour transformer les octets en mots que vous voulez vraiment, avec fatal: true quand les données comptent ; et Uint8Array.fromBase64 pour la voie moderne, stricte et rapide qui saute la chaîne entièrement. En entre, les variantes ont des noms et des règles : le base64url pour tout ce qui voyage dans une URL, un remplissage qui peut être présent ou non, des espaces que le vieux décodeur mange en silence. Et sous tout ça, deux attitudes : les octets ne sont pas le texte, et le texte n'est pas le secret. Décodez exprès, vérifiez avant de faire confiance, et quand le canal le permet, sautez le Base64 et prenez les octets.

L'autre moitié du voyage - prendre vos octets et votre texte et les transformer en la chaîne imprimable dont tout est parti - est couverte en détail dans le guide compagnon sur l'encodage Base64 en JavaScript, lié ci-dessous.

Dernière mise à jour : 2026-09-08

Article associé : Encodage Base64 en JavaScript/Browser : un guide complet