Encodage Base64 en JavaScript/Browser : un guide complet
Vous avez quelque chose à faire voyager, et la route n'est large que pour l'ASCII pur. Ce peut être une image qui doit entrer dans une réponse JSON, un objet de configuration qui doit monter à bord d'une URL, un token dont les trois segments ne sont que des points et des lettres, un fichier qu'une API exige en chaîne Base64 dans un corps JSON. Le Base64 est le péage de cette situation-là, et la page d'accueil de ce site a déjà présenté le format - quatre caractères imprimables qui tiennent la place de trois octets, avec un remplissage = pour finir le groupe - alors voici le seul nombre à garder en tête en lisant : l'encodage est la direction qui grandit. Chaque groupe de trois octets remis revient en quatre caractères, une taxe de taille d'environ 33 pour cent, collectée en bande passante, stockage et mémoire. Utilisez le Base64 quand le canal exige du texte imprimable, et sachez exactement ce que cette taxe vous coûte.
La nouvelle encourageante, c'est que le navigateur a toujours su faire ce travail sans le moindre package. btoa() est livré depuis le début des années 2000, TextEncoder a transformé votre vrai texte Unicode en honnêtes octets il y a une décennie, et dans la vague Baseline 2025, la plateforme a enfin ajouté Uint8Array.toBase64(), qui encode les tableaux d'octets directement, avec une option pour l'alphabet URL-safe. Cet article est la carte de décision : quel outil pour quel travail, où se cachent les arêtes vives (elles mènent toutes à la même frontière), et les recettes concrètes pour les endroits où vous serez vraiment demandé de produire du Base64.
Choisir votre encodeur
Il n'y a plus de « the » encodeur unique, et tendre la main vers le mauvais, c'est comme ça que naissent les bugs classiques. Le tableau ci-dessous est l'arbre de décision entier :
| Situation | Tenez-vous à |
|---|---|
| Texte ASCII pur, valeur ponctuelle | btoa(text) |
| Vrai texte avec accents, emoji, CJK | new TextEncoder().encode(text), puis btoa ou toBase64 |
Octets déjà dans un Uint8Array |
bytes.toBase64() dans les navigateurs de 2025 et plus, le pont btoa par morceaux ailleurs |
| URL, JWT, noms de fichiers | toBase64({ alphabet: 'base64url', omitPadding: true }) |
| Vieux navigateurs ou base de code partagée | js-base64, ou la recette classique TextEncoder + btoa |
Le motif sous le tableau : btoa() ne lit que des caractères mono-octet, donc tout ce qui n'est pas ASCII doit d'abord devenir un tableau d'octets, et c'est autour de ce tableau d'octets que les API modernes ont été construites. Gardez en tête « le texte devient octets, les octets deviennent Base64 », et chaque recette de cet article est ces mêmes deux étapes avec des noms différents dessus.
btoa et la frontière Latin1
btoa(stringToEncode) - de la chaîne binaire à la chaîne ASCII - est l'encodeur original, disponible dans tous les navigateurs qui comptent (Chrome 4, Firefox 1, Safari 3, IE 10 et plus, tous les contextes worker, et Node depuis la version 16). Son contrat a une clause, et c'est cette clause où tout déraille : chaque caractère de l'entrée doit avoir un point de code entre 0 et 255. La fonction lit des points de code, pas des octets UTF-8, donc « é » (point de code 233) passe au large pendant que « 你 » (point de code 20320) lève une DOMException nommée InvalidCharacterError avant qu'un seul caractère soit encodé. La frontière n'est pas « ASCII », ce n'est pas « Unicode », c'est précisément 256, et elle inclut les caractères de contrôle en bas - encoder un octet NUL est légal et a du sens, ce qui est l'une des raisons pour lesquelles la fonction existe du tout.
Le comportement complet, ligne par ligne :
| Entrée | Résultat |
|---|---|
"Hello, World!" |
"SGVsbG8sIFdvcmxkIQ==", le cas du manuel |
"" (chaîne vide) |
"", rien en entrée, rien en sortie |
"\u0000" (NUL) |
"AA==", les caractères de contrôle sont des citoyens de première classe |
"a\u00e9z" (é, point de code 233) |
"Yel6", toute la plage Latin1 passe |
"\u0100" (point de code 256) |
lève InvalidCharacterError, un pas au-delà de la frontière |
"h\u4f60" (你, point de code 20320) |
lève InvalidCharacterError, et c'est pareil pour tous les emoji, parce qu'ils sont tous bien au-dessus de 255 |
Deux notes pratiques. Le message d'erreur 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 » - donc dans tout code défensif vous attrapez sur le nom de l'exception. Et l'exception se lève sur le premier caractère fautif, pas à la fin : btoa n'encode pas la moitié de la chaîne et ne présente pas ses excuses. Quand vous voulez exprès le comportement Latin1 (encoder une chaîne d'octets construite délibérément à partir de points de code 0-255), la fonction fait exactement ce que vous avez demandé, et le tableau ci-dessus est toute sa personnalité.
Le pont des octets
La question devient donc : comment les données réelles - les octets UTF-8 de votre texte, le contenu d'un fichier, la sortie d'un canvas - arrivent-elles dans l'entrée de btoa() ? La réponse, c'est le « pont des octets » : une chaîne JavaScript où chaque caractère porte une valeur d'octet, la même astuce que produisent les décodeurs et que btoa comprend nativement. La version naïve est une boucle :
function bytesToBase64 (bytes) {
let binary = '';
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
Correct, mais la concaténation de chaînes dans une boucle est lente pour les gros fichiers, et le raccourci populaire - String.fromCharCode.apply(null, bytes), qui alimente tout le tableau en arguments en un seul appel - a un gouffre dur. Les appels de fonction ont une limite sur le nombre d'arguments, et elle est atteinte bien avant votre premier mégaoctet :
const big = new Uint8Array(1000000);
btoa(String.fromCharCode.apply(null, big));
// RangeError dans Firefox: "too many arguments provided for a function call"
// RangeError dans Chrome: "Maximum call stack size exceeded"
La correction qui a sauvé plus de fonctionnalités d'upload de fichiers que tout autre changement, c'est de traverser le pont par morceaux, quelques milliers de caractères à la fois, et de relier les résultats :
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(''));
}
Chaque morceau est assez petit pour appliquer en sécurité, subarray donne une vue sans copier, et le join produit exactement la même chaîne binaire que la boucle aurait produite. Maintenant l'autre face de la pièce, le texte. Pour n'importe quel vrai texte, TextEncoder - l'encodeur UTF-8 de la plateforme, disponible dans Firefox 18, Chrome 38, Safari 10.1 et partout depuis - transforme votre chaîne en honnêtes octets avant que le pont ne fasse son travail :
const bytes = new TextEncoder().encode('hello 你好');
const base64 = bytesToBase64Chunked(bytes);
console.log(base64); // "aGVsbG8g5L2g5aW9"
Cette sortie, c'est ce que « hello 你好 » est vraiment sur le fil : six octets ASCII plus six octets UTF-8 pour les deux caractères chinois, tous portant le même déguisement imprimable. Si votre texte n'est pas du UTF-8 - et sur le web, il l'est d'habitude - vous avez besoin de l'autre charset d'abord, ce qui veut dire l'encoder quelque part qui parle ce charset, d'habitude le serveur. TextEncoder refuse délibérément de deviner, et il a raison.
L'astuce de 2025 : Uint8Array.toBase64
Si vous tenez déjà un Uint8Array, le pont est un détour, parce que la nouvelle fonctionnalité ECMAScript (ES2026) encode le tableau directement : bytes.toBase64(options). Elle est arrivée dans Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 et Deno 2.5 - la même vague Baseline 2025 que sa sœur de décodage - et elle prend deux options qui en font l'encodeur le plus polyvalent de la plateforme. La première est alphabet : "base64" (la valeur par défaut) ou "base64url". La deuxième est omitPadding : mettez-la à true et les caractères = de la fin sont retirés, ce qui est la forme que la plupart des consommateurs qui aiment les URL veulent. Passer autre chose comme options lève une TypeError, ce qui est l'API qui est polie face à votre faute de frappe :
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="
Ces deux octets sont choisis pour être maximalement impolis avec l'alphabet : ils produisent un + et un / en mode standard, donc la dernière ligne montre exactement ce qui change quand on bascule en base64url. Les performances sont le bonus discret : sur un Firefox récent, encoder dix mégaoctets prend environ cinq millisecondes avec toBase64, alors que la voie du pont par chaînes ci-dessus prend environ quinze fois plus longtemps, parce qu'elle construit une gigantesque chaîne intermédiaire au passage. Sur les vieux navigateurs le pont reste parfaitement serviceable pour tout ce qui est sous quelques mégaoctets - et la version par morceaux ci-dessus est celle que vous voulez, pour les raisons de la dernière section.
Sortie URL-safe
Le Base64 a une variante dédiée pour les endroits où +, / et = causent des dégâts, et elle mérite sa propre section parce qu'autant de code cassé n'est que du Base64 standard qui a rencontré une URL. Dans une chaîne de requête, + est une espace ; dans un chemin, / est un séparateur ; et = veut un encodage en pourcentage à certaines positions. L'alphabet sûr pour les URL et les noms de fichiers du RFC 4648, section 5 - base64url - échange ces deux caractères contre - et _, et comme la longueur des données est d'habitude connue côté récepteur, il permet aussi de retirer le remplissage entièrement. La sortie voyage à travers URLSearchParams, les segments de chemin, les fragments et les noms de fichiers sans un seul signe pourcentage.
Avec l'API de 2025, c'est un objet d'options :
const params = new URLSearchParams();
params.set('payload', bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(params.toString()); // "payload=-_8" - aucun encodage en pourcentage du tout
Dans les vieux navigateurs, convertissez après avoir encodé avec btoa. Deux replaces et un trim font tout le travail :
function toUrlBase64 (base64) {
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
console.log(toUrlBase64(btoa('hi?/x'))); // "aGk_L3g"
Trois règles gardent le canal propre. Choisissez un alphabet par canal et tenez-vous-y - une valeur qui mélange + et - n'appartient à aucune famille, et aucun décodeur ne devinera laquelle vous vouliez dire. Le remplissage est un contrat, pas une suggestion : si vous l'omettez, le récepteur doit être prêt pour une valeur sans remplissage, et si vous le gardez, le récepteur ne doit pas s'étouffer dessus (les navigateurs sont indulgents, certains schémas JSON ne le sont pas). Et rappelez-vous que l'échange est réversible et sans perte - - et _ se mappent sur les mêmes positions 62 et 63 de l'alphabet que + et / occupent, donc rien n'est perdu en choisissant la paire plus amicale.
Faire voyager des images : les data URLs
L'usage le plus vieux et le plus visible du Base64 dans le navigateur est la data 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 - images, polices, audio - sont en Base64, et le navigateur les affiche avec zéro requête HTTP. Pour un fichier image que l'utilisateur vient de choisir, le FileReader fait l'encodage pour vous et rend l'URL finie :
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result); // "data:image/png;base64,iVBORw0KGgo..."
imageElement.src = reader.result;
};
reader.readAsDataURL(file);
Le résultat est un src prêt à l'emploi, une valeur que vous pouvez stocker dans localStorage, ou envoyer dans un corps JSON. Si l'image est sur un canvas à la place - une capture d'écran, une photo traitée, un graphique généré - canvas.toDataURL() fait ce travail depuis les toutes premières versions de navigateurs, et il vous laisse même choisir le format et, pour les formats avec perte, la qualité :
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);
Trois pièges à prévoir. D'abord, la règle du canvas contaminé : si vous avez dessiné une image cross-origin sur le canvas sans permission CORS, toute tentative de relire les pixels - y compris toDataURL - lève une SecurityError. La correction est de charger l'image avec crossOrigin = 'anonymous' et de s'assurer que le serveur envoie les bons en-têtes. Ensuite, l'argument qualité est ignoré pour le PNG et ne veut dire quelque chose que pour le JPEG (et le WebP) - une source commune de « pourquoi mon PNG est plus gros ». Troisième, et le plus gros : le payload est environ 33 pour cent plus gros que le fichier, et il vit dans la page sous forme de chaîne. Pour les images qui ne quittent jamais le navigateur, il y a une alternative gratuite - une object URL, qui enveloppe le Blob sans l'encoder du tout :
const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
URL.revokeObjectURL(objectUrl); // une fois que vous avez fini avec elle
Le partage des tâches qui en sort : object URLs pour tout ce qui reste sur la page, data URLs pour tout ce qui doit être copié, stocké ou envoyé en texte. Les deux sont de premier rang ; ils résolvent simplement des problèmes différents.
Construire et signer un JWT
Si vous générez des tokens dans le navigateur - pour un flux d'authentification auto-hébergé, une démo, ou un front end serverless - le format compact JWS est trois segments base64url : en-tête, payload, signature, sans remplissage nulle part. L'API Web Crypto gère la signature ; l'encodage est exactement la sortie URL-safe de deux sections plus tôt :
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;
Deux détails comptent plus que la tuyauterie. La signature couvre exactement header + '.' + payload - les segments bruts, pas le JSON - donc n'importe quelle édition d'une des deux parties invalide le token, ce qui est tout l'objet. Et crypto.subtle.sign renvoie un ArrayBuffer brut, d'où l'enveloppement d'une ligne dans un Uint8Array avant l'encodeur de segments. Pour les tokens basés sur RSA le flux est identique avec RS256 et une paire de clés, et si vous exportez une clé publique en JWK (crypto.subtle.exportKey('jwk', key)), les membres numériques - n, e, et pour les clés privées d, p, q - sortent automatiquement en base64url sans remplissage. Les réserves de sécurité sont les mêmes que pour n'importe quel token : un en-tête alg: "none" est une demande de sauter la vérification, les claims temporels (exp, nbf) doivent être imposés, et un serveur qui accepte à la fois HMAC et RSA pour la même audience ouvre la porte classique de la confusion de clés. Encodage correct, signature correcte, vérification du côté récepteur.
En-têtes d'authentification
Le schéma d'authentification le plus simple du web est aussi le plus instructif sur ce qu'est le Base64 et ce qu'il n'est pas. Le Basic HTTP envoie Authorization: Basic suivi du Base64 de username:password - un seul appel, pas de pont des octets nécessaire, parce que les noms d'utilisateur et les mots de passe sont (espérons-le) du texte brut :
const credentials = btoa('alice:secret123');
fetch('/api/me', {
headers: { Authorization: 'Basic ' + credentials }
});
// Authorization: Basic YWxpY2U6c2VjcmV0MTIz
Et voici la leçon qui tient en une ligne : le Base64 n'est pas un chiffrement. L'en-tête ci-dessus n'est qu'à un appel atob de alice:secret123 - pour l'attaquant et pour quiconque lit les logs - donc l'authentification Basic n'est acceptable que sur HTTPS, où le transport est la protection réelle et le Base64 n'est que le formatage. Pour tout ce qui a une vie plus longue qu'une requête, préférez les schémas basés sur des tokens : un token Bearer est aussi un seul en-tête, mais c'est une valeur aléatoire dont le secret n'a jamais besoin d'être porté dans l'en-tête, et qu'on peut révoquer. Le choix d'encodage entre les deux est trivial - les deux sont btoa ou du texte brut - mais le choix de sécurité ne l'est pas, et il devrait être fait exprès.
Fichiers en entrée, texte en sortie
Les uploads, c'est là que la taxe de 33 pour cent est chiffrée en vrai argent, parce que le fichier est d'habitude la plus grosse chose de la page. Il y a deux routes, et la première est celle que vous devriez prendre par défaut : les formulaires multipart. FormData porte le fichier en octets bruts dans un corps standard, avec le navigateur qui fait le cadrage, et il n'y a aucune trace de Base64 dans l'histoire - pas de taxe de taille, pas de chaîne intermédiaire, et les octets partent en flux vers le serveur au fur et à mesure qu'ils sont lus :
const form = new FormData();
form.append('upload', file);
await fetch('/api/upload', { method: 'POST', body: form });
La seconde route est pour les API qui insistent sur un corps JSON avec le fichier en chaîne - certaines fonctions serverless, certains backends mobiles, certains services hérités. Là, l'encodage tient en une ligne par fichier, et le coût est exactement ce que dit la taxe : un fichier de 5 mégaoctets devient une chaîne de 6,7 mégaoctets, qui est alors sérialisée en JSON, qui est alors envoyée. Correct pour une photo, douloureux pour une vidéo :
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
});
Pour les gros fichiers sur cette route, ne construisez pas une gigantesque chaîne en un seul appel - construisez-la en tranches, où chaque tranche est un multiple de trois octets. C'est cet alignement qui rend l'astuce légale : un multiple de trois octets s'encode en un multiple propre de quatre caractères sans remplissage, donc les tranches encodées indépendamment se concatènent en exactement l'encodage du fichier entier, et seule la tranche finale porte du remplissage :
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('');
}
La même idée d'alignement explique pourquoi vous ne devez jamais couper une chaîne Base64 à un point arbitraire et attendre que les morceaux se décodent seuls - un groupe de trois octets est l'atome, et une coupure au milieu d'un groupe laisse un fragment en suspens. Les téléchargements sont l'image miroir : pour un fichier généré, un petit peut sortir via une data URL sur un lien de téléchargement, mais pour tout ce qui a une vraie taille, un Blob plus une object URL est la voie saine, parce que le navigateur n'a jamais à porter tout le payload en chaîne en premier lieu.
Stocker et partager l'état
Deux canaux texte uniquement de plus où le Base64 fait un vrai travail. Le premier est le stockage : localStorage et sessionStorage tiennent des chaînes, donc les données structurées ou binaires sont encodées avant d'entrer. L'aller-retour tient en un encodage et un décodage, et il vaut la peine de voir les deux côtés ensemble, parce qu'un bug de stockage est presque toujours un désaccord de charset entre eux :
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));
Budgétez pourtant proprement : l'origine reçoit environ 5 mégaoctets de localStorage, votre chaîne stockée est 33 pour cent plus graisseuse que les données, et pendant que la page est ouverte la chaîne vit aussi en mémoire en UTF-16 - deux fois sa longueur encore. Un actif de 3 mégaoctets est 4 mégaoctets de stockage et 8 mégaoctets de mémoire, c'est comme ça qu'une fonctionnalité « petite » devient une erreur de quota. Le second canal est l'URL elle-même : liens de partage, deep links et état OAuth veulent tous des données structurées dans un endroit qui survit au copier-coller. La recette : état compact, JSON, puis base64url sans remplissage, pour que la valeur n'ait besoin d'aucun encodage en pourcentage - et gardez l'URL entière sous quelques milliers de caractères, c'est là que les vieux clients, les proxies et les outils de logs commencent à s'inquiéter.
E-mail et MIME
Le Base64 est plus vieux que le web, et son terrain de jeu est l'e-mail. Les pièces jointes MIME avec Content-Transfer-Encoding: base64, c'est comme ça qu'un fichier binaire voyage à l'intérieur d'un protocole texte, et la convention qui en a découlé de l'ancienne limite de 76 caractères par ligne du format de message vaut la peine d'être connue : envelopper le corps encodé à 76 caractères par ligne. Le navigateur ne peut pas envoyer de SMTP, mais il fait deux travaux d'e-mail - construire des corps MIME qu'un relais backend enverra, et afficher les pièces jointes des messages qu'il reçoit - et les deux touchent l'encodage. L'enveloppement lui-même est une fonction de deux lignes, et l'ordre des opérations compte : d'abord encoder, ensuite envelopper, parce que btoa ne lèvera pas d'exception sur un retour à la ligne dans son entrée - il encodera la coupure comme un octet de payload, et vos retours à la ligne se retrouveront dans la sortie :
function wrapForMime (base64, width) {
const w = width || 76;
return base64.match(new RegExp('.{1,' + w + '}', 'g')).join('\r\n');
}
Le côté récepteur est le gratuit : atob saute les espaces ASCII comme partie de son comportement standard, donc un corps MIME enveloppé se décode exactement comme il est arrivé, retours à la ligne compris, sans étape de déboulage. Si vous construisez un client webmail ou un sélecteur de pièces jointes, cette asymétrie unique - l'encodeur doit produire des lignes propres, le décodeur s'en moque - est toute l'histoire du MIME en une phrase.
Quand prendre une bibliothèque
Avec les outils natifs ci-dessus, une bibliothèque est rarement nécessaire, et la guidance honnête est : par défaut la plateforme, et ajouter un package seulement quand une vraie exigence pointe dessus. Les trois qui apparaissent vraiment dans les bases de code :
js-base64 (npm install js-base64) est le polyvalent : un petit transcodeur en JavaScript pur qui traite les chaînes UTF-8 comme des citoyens de première classe - Base64.encode sur une chaîne CJK fait la danse UTF-8 pour vous - et, utile pour le décodage autant que pour l'encodage, accepte les deux alphabets dans decode et embarque une vérification isValid. C'est la bonne réponse quand vous ciblez des navigateurs où les API de 2025 manquent et que vous voulez un seul import pour couvrir chaînes et octets :
import { Base64 } from 'js-base64';
const encoded = Base64.encode('小飼弾'); // "5bCP6aO85by+" - UTF-8 géré pour vous
const decoded = Base64.decode('5bCP6aO85by-'); // lit standard et URL-safe pareil
const valid = Base64.isValid(encoded); // true
base64-js est celui axé octets : fromByteArray et toByteArray sur des Uint8Array, aucune dépendance, le cheval de trait du vieux écosystème browserify et toujours un bon choix quand votre code vit dans des typed arrays et que vous voulez que l'encodage soit une pure fonction des octets. Et si votre raison de vouloir une bibliothèque est « j'aime l'API de 2025 mais je ne peux pas exiger des navigateurs 2025 », la réponse n'est pas un package Base64 du tout mais un polyfill : core-js (et le preset Babel qui le tire) implémente Uint8Array.fromBase64 et ses amis, donc vous pouvez écrire le code à la nouvelle manière une fois et laisser le shim combler le trou sur les vieux moteurs. Choisissez par contrainte - vieux navigateurs, confort de chaînes, ou pureté d'octets - pas par habitude.
Les pièges qui coûtent des heures aux développeurs
- Appeler
btoasur une chaîne avec un caractère au-dessus du point de code 255. Elle lève, elle ne déforme pas, et elle s'arrête au premier fautif. La correction est toujours la même :TextEncoderd'abord, pont ensuite. - Le gouffre de
fromCharCode.applysur les gros tableaux. Un million d'arguments, c'est uneRangeErrordans les deux grands moteurs. Morcellez le pont, ou passez àtoBase64. - Oublier la taxe de taille là où ça fait le plus mal : le stockage. Un fichier dans
localStorageest 33 pour cent plus gros que le fichier, et le quota est par origine, partagé avec tout le reste que votre application stocke. - Le Base64 standard qui rencontre une chaîne de requête. Le
+arrive comme une espace, le/casse le chemin, et les signalements disent « l'API est instable ». Sortie URL-safe, sans remplissage, et tout le genre de bug disparaît. - Remplissage incohérent entre services. Une passerelle garde les
=, une autre les retire, une troisième les rajoute. Le récepteur doit être prêt pour les deux formes, et le contrat devrait dire laquelle est canonique. - Traiter le Base64 comme un cadenas. C'est un format de sérialisation, à un seul appel de fonction du texte en clair, et « encodé en Base64 » dans une revue de sécurité est un constat, pas un contrôle.
- Les chaînes binaires comme modèle de mémoire. Un mégaoctet décodé ou encodé voyage en UTF-16 à deux mégaoctets ; un
Uint8Arrayle tient à un. Pour les gros payloads, gardez les octets dans des typed arrays de bout en bout. - L'encodage double. Une valeur qui était déjà du Base64 est encodée encore une fois, et le consommateur décode une fois et obtient une chaîne de lettres au lieu de données. En cas de doute, vérifiez avant d'envelopper - une chaîne qui est déjà dans l'alphabet avec un remplissage valide est une odeur.
- Faire confiance au payload d'un JWT parce qu'il s'est décodé proprement. La décodabilité n'est pas l'authenticité. Vérifiez la signature avec la bonne clé et le bon algorithme avant de lire le moindre claim.
Performance : ce que coûtent un million d'octets
Le Base64 dans le navigateur est bon marché là où il était cher, et le budget a maintenant trois lignes au lieu d'une. CPU : sur un Firefox récent, Uint8Array.toBase64 encode dix mégaoctets en environ cinq millisecondes, alors que le pont btoa par morceaux prend environ quinze fois plus longtemps - pas parce que btoa est lent, mais parce que le pont construit une gigantesque chaîne intermédiaire au passage. Si votre budget d'encodage est en millisecondes, utilisez la méthode native ; si vous encodez un objet de configuration de 2 kilo-octets, les deux sont sous le seuil de perception. Bande passante : c'est la taxe permanente - chaque octet encodé coûte 1,33 octet sur le fil, plus le cadrage que le transport ajoute. Mesurez le transfert avant de « optimiser » l'encodage. Mémoire : la chaîne encodée est la plus grosse allocation transitoire que vous ferez, et pour un fichier de 5 mégaoctets c'est une chaîne de 6,7 mégaoctets, soit environ 13,4 mégaoctets de mémoire UTF-16 pendant que la page la tient. Les conséquences pratiques tombent de l'arithmétique : tranchez les gros encodages pour qu'aucune chaîne seule ne devienne énorme, libérez les octets intermédiaires dès que la chaîne existe, préférez object URLs et multipart quand les octets n'avaient jamais besoin d'être imprimables, et déplacez le travail multi-mégaoctets dans un Web Worker si le thread principal doit continuer à faire défiler sans à-coups. Le format a près de quatre décennies ; la plateforme vient enfin de le rattraper.
Comment les navigateurs ont appris à encoder
L'encodeur a une histoire, et elle explique les reliques que vous hériterez. btoa - « de binaire à ASCII », le nom est littéral, et atob est juste les mêmes mots à l'envers - a été écrit dans la spécification HTML en 2011, rétro-ingéniéré à partir des navigateurs qui l'avaient déjà livré : Firefox depuis 2004, Safari 3, Chrome 4. Internet Explorer, caractéristiquement, a sauté les deux fonctions jusqu'à la version 10 en 2012, et cette absence unique est la raison pour laquelle une décennie de JavaScript regorge de tables Base64 faites à la main et d'une invocation toute particulière pour l'Unicode : btoa(unescape(encodeURIComponent(str))). Ça marchait - encodeURIComponent produit du UTF-8 échappé en pourcentage, et unescape transformait ça en chaîne d'octets - mais ça reposait sur unescape(), celui de la paire que le langage avait déprécié, et il a survécu dans le code des navigateurs pendant des années par pure inertie. La correction principielle est arrivée avec la norme Encoding : TextEncoder et TextDecoder, dans Firefox 18 (2013), Chrome 38 (2014), Safari 10.1 (2017), et dans aucune version d'IE du tout - encore un trou IE, encore une décennie de contournements. Node.js raconte la moitié côté serveur de l'histoire : il avait Buffer avec le Base64 dès le premier jour, mais atob et btoa comme globales seulement depuis la version 16 en 2021, avec deux petits shims npm qui portaient la charge avant cela. Et puis, entre la fin 2024 et 2025, le langage lui-même a livré le Base64 - Uint8Array.toBase64 et ses amis dans Firefox 133 (novembre 2024), Safari 18.2 (décembre 2024), Chrome 140 (septembre 2025) et Node 25 (octobre 2025) - et la fonctionnalité a été marquée Baseline 2025 - le même set de fonctionnalités que la plateforme approchait avec des utilitaires depuis vingt ans, maintenant standard. Les anecdotes amusantes à la fin de l'article sont surtout sur le temps que chaque morceau a pris à arriver.
Saviez-vous ?
- Les noms des fonctions sont une phrase :
btoa, c'est « de binaire à ASCII », etatob, c'est « d'ASCII à binaire ». La direction est dans le nom, c'est pourquoi le duo est auto-documenté depuis les années 2000. - La chaîne la plus encodée de l'histoire de l'informatique est probablement « hello » :
btoa('hello')donneaGVsbG8=, la sortie de chaque tutoriel, de chaque suite de tests et de chaque tableau blanc d'entretien sur la planète. - Toute chaîne Base64 valide a une longueur multiple de quatre, remplissage compris. Les caractères
=sont une empreinte : un d'entre eux veut dire que le dernier groupe contenait deux octets, deux veulent dire qu'il en contenait un. - L'enveloppement de 76 caractères par ligne dans le MIME et dans la plupart des outils en ligne de commande est un héritage de l'ère de l'e-mail, quand la longueur de ligne du format de message fixait la limite. Le nombre a survécu à trois décennies de tout-va-plus-vite.
- « Data URI » est un nom à la retraite. Le WHATWG l'a renommé en « data URL » pendant la grande harmonisation URI-vers-URL, c'est pourquoi les spécifications, les articles de blog et les noms de packages l'écrivent tous différemment dans le même paragraphe.
btoa('')renvoie'': une entrée vide produit une sortie vide, sans remplissage, sans cas particulier - la seule chaîne Base64 avec zéro caractère (sa longueur, 0, est encore un multiple de quatre).- Un canvas peut transformer une photo en data URL avec
toDataURL- une capacité qui existe depuis IE 9, Firefox 2 et Safari 4, antérieure à la plupart de la plateforme web que nous prenons pour « moderne » - et la faire aller-retour avec une balise<img>et unFileReader. - Le handshake WebSocket encode
SHA-1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)en Base64, et le GUID est une constante fixe du RFC, choisie précisément pour qu'aucun serveur HTTP pur ne puisse jamais accomplir le handshake par accident.
Où aller d'ici
Tout l'art de l'encodage dans le navigateur tient sur une page : btoa pour les cas purs, mono-octet, pour lesquels il est né ; TextEncoder plus le pont par morceaux pour le vrai texte et les fichiers sur n'importe quel navigateur ; Uint8Array.toBase64 avec ses options d'alphabet et de remplissage pour la voie moderne et directe ; et la variante URL-safe, avec ou sans remplissage, pour tout ce qui vivra dans une URL. Le reste est du jugement : connaissez la taxe de 33 pour cent avant de la payer, gardez les octets dans des typed arrays tant qu'ils sont gros, encodez d'abord et enveloppez ensuite, et n'appellez jamais un format de sérialisation un cadenas. Quand le canal peut porter des octets bruts, prenez les octets - le Base64 est pour les routes qui n'admettent que du texte imprimable, et maintenant vous savez exactement comment payer le péage.
L'autre moitié du voyage - recevoir une de ces chaînes et en tirer à nouveau les octets, le texte et le sens - est couverte en détail dans le guide compagnon sur le décodage Base64 en JavaScript, lié ci-dessous.
Dernière mise à jour : 2026-09-08
Article associé : Décodage Base64 en JavaScript/Browser : un guide complet