[JS] Base64 в JavaScript — atob, btoa и Buffer в Node
Практическое руководство по Base64 в JavaScript. Когда использовать btoa/atob, почему они ломаются на Unicode, как работает Buffer в Node.js и переносимый хелпер для base64url.
// ПЕЙЗАЖ
История JavaScript с Base64 разрознена исторически. Браузер унаследовал btoa() / atob() от Netscape из 1990-х — функции, названия которых буквально означают binary-to-ASCII и ASCII-to-binary. Они умеют работать только с символами Latin-1, поэтому копирование эмодзи или китайского текста через btoa() бросает исключение.
В Node.js появился свой путь через класс Buffer. Он поддерживает все нужные вам кодировки (utf8, base64, base64url, hex, latin1, ascii), работает с двоичными данными нативно и является очевидным выбором для серверного кода.
Современные рантаймы (Node 18+, Deno, Bun, актуальные браузеры) также поставляются с TextEncoder / TextDecoder — стандартизированным способом превращать Unicode-строки в байты перед кодированием в Base64. Умение выбрать правильный инструмент — половина дела.
// БРАУЗЕР: btoa / atob — КОРРЕКТНОЕ ПРИМЕНЕНИЕ
btoa(str) принимает строку, где каждый символ помещается в 1 байт (диапазон Latin-1, U+0000 … U+00FF), и возвращает строку Base64. atob(b64) делает обратное. Они синхронные, быстрые и встроены в каждый браузер с незапамятных времён.
Корректный сценарий: применять Base64 к данным, уже представленным как байты. Сырые байты, прочитанные из FileReader, из бинарного XMLHttpRequest (responseType: 'arraybuffer') или из Uint8Array, где числовое значение каждого байта укладывается в 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=='
// ПОЧЕМУ btoa() ПАДАЕТ НА UNICODE
Попробуйте в консоли браузера btoa('héllo'). Вы получите InvalidCharacterError: The string to be encoded contains characters outside of the Latin1 range. Символ é (U+00E9) ещё проходит, но btoa('日本語') падает, потому что 日 (U+65E5) за пределами Latin-1. Функция ничего не знает про UTF-8; она читает каждую кодовую единицу JavaScript-строки как один байт.
Решение — сначала превратить Unicode-строку в байты UTF-8, а затем закодировать эти байты в Base64. В любом современном браузере для этого есть TextEncoder.
// ❌ 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: ПОДХОД НА БАЗЕ Buffer
В Node берите Buffer. Он по умолчанию понимает UTF-8, поддерживает все мыслимые кодировки и чисто работает с бинарём. Начиная с Node 16 он понимает и имя кодировки 'base64url', так что отдельный хелпер для URL-безопасного вывода не нужен.
// 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 ПРОТИВ btoa/atob — БЫСТРОЕ СРАВНЕНИЕ
- > Unicode: Buffer поддерживает UTF-8 нативно; btoa/atob умеют только Latin-1.
- > Бинарь: Buffer — нативный бинарный тип в Node; в браузере приходится использовать Uint8Array + String.fromCharCode.
-
>
URL-safe: Buffer поддерживает
'base64url'как полноценную кодировку (Node 16+); в браузере нужна своя regex-замена. - > Производительность: Buffer реализован на C++ и на больших входах в 3–5× быстрее, чем atob/btoa.
- > Стриминг: Buffer можно прогонять через transform-потоки для больших файлов; btoa/atob потоково не умеют.
- > Доступность: btoa/atob есть в Node 16+ как глобальные, но при возможности берите Buffer — он ровнее справляется с крайними случаями.
// КОДИРОВАНИЕ И ДЕКОДИРОВАНИЕ БИНАРНЫХ ФАЙЛОВ В БРАУЗЕРЕ
Канонический браузерный паттерн превращения выбора из файл-пикера в Base64-строку — это FileReader.readAsDataURL(), который сразу возвращает готовый data URI. Если нужна только «сырая» полезная нагрузка Base64, уберите префикс data:...;base64,.
Для очень больших файлов читайте через readAsArrayBuffer() и кодируйте по частям, чтобы не получить «maximum call stack size exceeded» от String.fromCharCode(...bytes) на массивах длиннее ~65 000 элементов.
// ▸ 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);
// ДЕКОДИРОВАНИЕ BASE64 В БАЙТЫ В БРАУЗЕРЕ
// ▸ 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);
// URL-БЕЗОПАСНЫЙ BASE64 (base64url) В JAVASCRIPT
Стандартный Base64 использует + и / — оба являются зарезервированными символами в URL. В конце также могут стоять один-два =, которые процентно кодируются в %3D, зря занимая место. RFC 4648 §5 определяет URL-безопасный вариант (base64url): заменяем + на -, / на _ и убираем паддинг.
В Node 16+ просто пишите Buffer.from(x).toString('base64url'). В браузере реализуйте это как постобработку результата 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/'
// ЧАСТЫЕ ЛОВУШКИ И КАК ИХ ЛОВИТЬ
-
>
InvalidCharacterError из btoa() — во входе есть символы за пределами Latin-1. Лечение: сначала переведите в UTF-8 через
TextEncoder. -
>
InvalidCharacterError из atob() — вход не является валидным Base64. Обычно это отсутствующий паддинг (для base64url) или пробельные символы. Уберите пробелы, верните паддинг
=. -
>
Искажённый Unicode при декодировании — вы декодировали напрямую через
atob(), а не черезTextDecoder. Многобайтовые последовательности UTF-8 превращаются в кракозябры. -
>
'Maximum call stack size exceeded' — вы применили
String.fromCharCode(...bytes)к массиву длиннее ~65 000 байт. Лечение: разбейте на куски (см. цикл по 32 КБ выше). - > Разный вывод в браузере и Node — обычно значит, что одна сторона использует URL-safe, а другая — стандартный Base64. Нормализуйте к одному варианту на границе.
-
>
Двойное кодирование — данные закодированы в Base64 дважды. Проверка: выглядит ли
atob(input)снова как Base64? Тогда раскодируйте ещё раз. - > Base64 в JSON случайно проэкранирован — вы обернули Base64 в JSON.stringify, получатель разобрал JSON и увидел строку как есть, но ваш серверный код ошибочно закодировал её снова. Проверьте реальный сетевой payload.
// ПЕРЕНОСИМЫЙ ХЕЛПЕР (браузер + 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 世界'
// ЗАМЕТКИ О ПРОИЗВОДИТЕЛЬНОСТИ
- > Buffer — самый быстрый — в Node он исполняется в C++ и не задыхается на мегабайтных входах.
- > btoa/atob конкурентоспособны для небольших нагрузок (< 100 КБ) в браузере; оба синхронные и на большом входе заблокируют главный поток.
-
>
Для > 10 МБ в браузере запускайте кодирование в Worker — или используйте
Blob.text()+FileReader.readAsDataURL(), которые оптимизированы внутри. - > Избегайте повторных циклов encode/decode — каждый проход — O(n) по времени и памяти. Если Base64 используется несколько раз, закэшируйте его.
-
>
Стриминг — для по-настоящему больших нагрузок (100 МБ+) используйте
stream.Transformв Node или APITransformStreamв браузере с потоковым Base64-энкодером.
// СМЕЖНЫЕ МАТЕРИАЛЫ
• Что такое Base64 и как он работает? — приём 6-битной группировки с нуля.
• Base64 для UTF-8 и Unicode — танец с TextEncoder в подробностях.
• URL-safe и стандартный Base64 — когда нужны -_ вместо +/.
• Попробуйте живой кодировщик Base64 — вставьте текст, посмотрите кодирование, скопируйте результат.
Сохраните эту страницу в закладки, если время от времени тянетесь к btoa() — в девяти случаях из десяти ответ на ваш вопрос уже здесь.