[JS] Base64 en JavaScript — atob, btoa et Buffer Node
Un guide pratique du Base64 en JavaScript. Quand utiliser btoa/atob, pourquoi ils échouent sur l'Unicode, comment Buffer fonctionne dans Node.js, et un utilitaire base64url multi-runtime.
// LE PAYSAGE
L'histoire de JavaScript avec le Base64 est fragmentée par l'héritage. Le navigateur a hérité de btoa() / atob() de Netscape dans les années 1990 — des fonctions dont les noms signifient littéralement binary-to-ASCII et ASCII-to-binary. Elles ne gèrent que les caractères Latin-1, raison pour laquelle coller des emojis ou du texte chinois dans btoa() lève une exception.
Node.js a ajouté sa propre voie via la classe Buffer. Elle comprend tous les encodages dont vous êtes susceptible d'avoir besoin (utf8, base64, base64url, hex, latin1, ascii), gère les données binaires nativement, et constitue le choix évident pour le code côté serveur.
Les runtimes modernes (Node 18+, Deno, Bun, navigateurs modernes) fournissent aussi TextEncoder / TextDecoder — la méthode normalisée pour transformer des chaînes Unicode en octets avant de les encoder en Base64. Savoir quel outil choisir représente la moitié de la bataille.
// NAVIGATEUR : btoa / atob — L'USAGE CORRECT
btoa(str) prend une chaîne dont chaque caractère doit tenir sur 1 octet (plage Latin-1, U+0000 à U+00FF) et renvoie une chaîne Base64. atob(b64) inverse l'opération. Elles sont synchrones, rapides, et intégrées à chaque navigateur depuis toujours.
L'usage correct est : encoder en Base64 des données qui sont déjà assimilables à des octets. Des octets bruts lus depuis un FileReader, depuis une XMLHttpRequest binaire (responseType: 'arraybuffer'), ou depuis un Uint8Array, où la valeur numérique de chaque octet tient dans 0–255.
// Encode a Latin-1 string
const latin1 = 'Hello, world!';
const encoded = btoa(latin1); // 'SGVsbG8sIHdvcmxkIQ=='
// Decode it back
const decoded = atob(encoded); // 'Hello, world!'
// Encode a binary Uint8Array (the common real-world case)
const bytes = new Uint8Array([0xFF, 0xD8, 0xFF, 0xE0]); // JPEG magic
const binStr = String.fromCharCode(...bytes); // 'ÿØÿà' (Latin-1)
const b64 = btoa(binStr); // '/9j/4A=='
// POURQUOI btoa() ÉCHOUE SUR L'UNICODE
Essayez btoa('héllo') dans une console de navigateur. Vous obtenez InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range. Le é (U+00E9) passe, mais btoa('日本語') échoue parce que 日 (U+65E5) est hors Latin-1. La fonction n'a aucune notion d'UTF-8 ; elle lit chaque unité de code d'une String JavaScript comme un octet unique.
Le correctif consiste à transformer d'abord la chaîne Unicode en octets UTF-8, puis à encoder ces octets en Base64. Tout navigateur moderne fournit TextEncoder exactement pour cela.
// ❌ broken
// btoa('日本語') → throws InvalidCharacterError
// ✅ correct: encode to UTF-8 first
function b64encodeUtf8(str) {
const bytes = new TextEncoder().encode(str);
let bin = '';
for (const byte of bytes) bin += String.fromCharCode(byte);
return btoa(bin);
}
b64encodeUtf8('日本語'); // '5pel5pys6Kqe'
// ✅ round-trip decode
function b64decodeUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
b64decodeUtf8('5pel5pys6Kqe'); // '日本語'
// NODE.JS : L'APPROCHE Buffer
Dans Node, tournez-vous vers Buffer. Il est UTF-8 par défaut, prend en charge tous les encodages imaginables, et gère le binaire proprement. Node 16+ comprend aussi 'base64url' comme nom d'encodage, si bien que vous n'avez pas besoin d'un utilitaire séparé pour la sortie URL-safe.
// String → Base64
const encoded = Buffer.from('héllo 世界').toString('base64');
// 'aMOpbGxvIOS4lueVjA=='
// Base64 → string (UTF-8 by default)
const decoded = Buffer.from(encoded, 'base64').toString();
// 'héllo 世界'
// URL-safe (Node 16+): +/- omitted padding
const safe = Buffer.from('héllo 世界').toString('base64url');
// 'aMOpbGxvIOS4lueVjA' ← no = padding, uses -_
// Binary file → Base64
const fs = require('node:fs');
const imgB64 = fs.readFileSync('logo.png').toString('base64');
// Base64 → binary file
fs.writeFileSync('out.png', Buffer.from(imgB64, 'base64'));
// BUFFER VS btoa/atob — COMPARATIF RAPIDE
- > Unicode : Buffer gère l'UTF-8 nativement ; btoa/atob ne gèrent que des octets Latin-1.
- > Binaire : Buffer est le type binaire natif dans Node ; les navigateurs doivent utiliser Uint8Array + String.fromCharCode.
-
>
URL-safe : Buffer prend en charge
'base64url'comme encodage de première classe (Node 16+) ; les navigateurs ont besoin d'un remplacement par expression régulière personnalisé. - > Performance : Buffer est implémenté en C++ et est environ 3 à 5× plus rapide que atob/btoa sur de grandes entrées.
- > Streaming : Buffer peut être acheminé via des flux de transformation pour les gros fichiers ; btoa/atob ne peuvent pas être utilisés en flux.
- > Disponibilité : btoa/atob existent dans Node 16+ en tant que globales, mais privilégiez Buffer quand vous avez le choix — il gère les cas limites.
// ENCODAGE ET DÉCODAGE DE FICHIERS BINAIRES DANS LE NAVIGATEUR
Le modèle canonique du navigateur pour transformer une sélection de fichier en chaîne Base64 est FileReader.readAsDataURL(), qui renvoie un data URI prêt à l'emploi. Si vous ne voulez que la charge utile Base64 brute, supprimez le préfixe data:...;base64,.
Pour les très gros fichiers, lisez avec readAsArrayBuffer() et encodez par blocs pour éviter l'erreur « maximum call stack size exceeded » que String.fromCharCode(...bytes) déclenche sur des tableaux de plus d'environ 65 000 éléments.
// ▸ Simple: file → data URI → strip prefix
async function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataUri = reader.result; // 'data:image/png;base64,iVBOR...'
resolve(dataUri.split(',')[1]); // raw Base64 only
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// ▸ Large files: chunked btoa() to avoid stack overflow
async function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000; // 32 KB chunks
let binary = '';
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode.apply(null,
bytes.subarray(i, i + chunkSize));
}
return btoa(binary);
}
// Usage
const file = inputEl.files[0];
const buf = await file.arrayBuffer();
const b64 = await arrayBufferToBase64(buf);
// DÉCODER DU BASE64 VERS DES OCTETS DANS LE NAVIGATEUR
// ▸ Base64 → Uint8Array (binary bytes)
function base64ToBytes(b64) {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
// ▸ Base64 → Blob → downloadable
function base64ToBlob(b64, mime = 'application/octet-stream') {
const bytes = base64ToBytes(b64);
return new Blob([bytes], { type: mime });
}
// Trigger a download
const blob = base64ToBlob(b64, 'image/png');
const url = URL.createObjectURL(blob);
Object.assign(document.createElement('a'), { href: url, download: 'out.png' }).click();
URL.revokeObjectURL(url);
// BASE64 URL-SAFE (base64url) EN JAVASCRIPT
Le Base64 standard utilise + et /, deux caractères réservés dans les URL. Il se termine également par un ou deux = qui sont encodés en pourcentage en %3D, gaspillant de l'espace. La RFC 4648 §5 définit la variante URL-safe (base64url) : remplacer + par -, / par _, et supprimer le remplissage.
Dans Node 16+, utilisez simplement Buffer.from(x).toString('base64url'). Dans le navigateur, implémentez-la comme une étape de post-traitement au-dessus de btoa().
// ▸ Browser: standard Base64 → URL-safe Base64
function toBase64Url(b64) {
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// ▸ URL-safe Base64 → standard Base64 (before feeding atob)
function fromBase64Url(b64u) {
let b64 = b64u.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '='; // re-add padding
return b64;
}
// Round-trip
const token = toBase64Url(btoa('hello+world/'));
// 'aGVsbG8rd29ybGQv'
atob(fromBase64Url(token)); // 'hello+world/'
// PIÈGES COURANTS & COMMENT LES REPÉRER
-
>
InvalidCharacterError depuis btoa() — votre entrée contient des caractères hors Latin-1. Correctif : encodez d'abord en UTF-8 avec
TextEncoder. -
>
InvalidCharacterError depuis atob() — votre entrée n'est pas du Base64 valide. Généralement un remplissage manquant (pour base64url), ou des espaces. Supprimez les espaces, rajoutez le remplissage
=. -
>
Unicode tronqué au décodage — vous avez décodé avec
atob()directement au lieu de passer parTextDecoder. Les séquences multi-octets UTF-8 apparaissent sous forme de mojibake. -
>
« Maximum call stack size exceeded » — vous avez utilisé
String.fromCharCode(...bytes)sur un tableau de plus d'environ 65 000 octets. Correctif : découpez-le en blocs (voir la boucle de 32 Ko ci-dessus). - > Sortie différente entre le navigateur et Node — signifie généralement qu'un côté est en URL-safe et l'autre en standard. Normalisez sur une seule variante à la frontière.
-
>
Charge utile doublement encodée — les données ont été encodées deux fois en Base64. Vérifiez :
atob(input)ressemble-t-il à nouveau à du Base64 ? Alors décodez une fois de plus. - > Base64 dans JSON accidentellement encodé en JSON — vous avez enveloppé une chaîne Base64 dans JSON.stringify, puis le destinataire analyse le JSON et obtient la chaîne inchangée, mais votre code serveur l'a réencodée par erreur. Inspectez la charge utile sur le fil.
// UTILITAIRE MULTI-RUNTIME (navigateur + Node + Deno + Bun)
// A portable Base64/base64url helper. Uses Buffer when available,
// falls back to btoa/atob + TextEncoder in the browser / Deno.
const hasBuffer = typeof Buffer !== 'undefined';
export function encode(str, { urlSafe = false } = {}) {
if (hasBuffer) {
return Buffer.from(str, 'utf8').toString(urlSafe ? 'base64url' : 'base64');
}
const bytes = new TextEncoder().encode(str);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
const b64 = btoa(bin);
return urlSafe
? b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
: b64;
}
export function decode(b64, { urlSafe = false } = {}) {
if (hasBuffer) {
return Buffer.from(b64, urlSafe ? 'base64url' : 'base64').toString('utf8');
}
if (urlSafe) {
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
}
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
// Usage
encode('héllo 世界'); // 'aMOpbGxvIOS4lueVjA=='
encode('héllo 世界', { urlSafe: true }); // 'aMOpbGxvIOS4lueVjA'
decode('aMOpbGxvIOS4lueVjA=='); // 'héllo 世界'
// NOTES DE PERFORMANCE
- > Buffer est le plus rapide — dans Node, il s'exécute en C++ et gère des entrées de plusieurs mégaoctets sans broncher.
- > btoa/atob sont compétitifs pour les petites charges utiles (< 100 Ko) dans le navigateur ; les deux sont synchrones et bloqueront le thread principal sur de grandes entrées.
-
>
Pour les entrées > 10 Mo dans le navigateur, utilisez un Worker pour que l'encodage s'exécute en dehors du thread principal — ou utilisez
Blob.text()+FileReader.readAsDataURL(), optimisés en interne. - > Évitez les cycles encodage/décodage répétés — chaque aller-retour est en O(n) en temps et en mémoire. Mettez en cache la forme Base64 si vous l'utilisez plusieurs fois.
-
>
Streaming — pour des charges utiles vraiment volumineuses (100 Mo+), utilisez
stream.Transformde Node ou l'APITransformStreamdu navigateur avec un encodeur Base64 par blocs.
// LECTURES CONNEXES
• Qu'est-ce que le Base64 et comment fonctionne-t-il ? — l'astuce du regroupement sur 6 bits expliquée depuis zéro.
• Base64 pour UTF-8 et Unicode — la danse TextEncoder en profondeur.
• URL-safe vs Base64 standard — quand vous avez besoin de -_ au lieu de +/.
• Essayez l'encodeur Base64 en direct — collez du texte, voyez l'encodage, copiez le résultat.
Mettez cette page en favori si vous êtes amené à utiliser btoa() — neuf fois sur dix, la question que vous êtes sur le point de googler se trouve ici.