[JS] 10 мин чтения

[JS] Base64 в JavaScript — atob, btoa и Buffer в Node

Практическое руководство по Base64 в JavaScript. Когда использовать btoa/atob, почему они ломаются на Unicode, как работает Buffer в Node.js и переносимый хелпер для base64url.

Апрель 2026 | javascript

// ПЕЙЗАЖ

История 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 или API TransformStream в браузере с потоковым Base64-энкодером.

// СМЕЖНЫЕ МАТЕРИАЛЫ

Что такое Base64 и как он работает? — приём 6-битной группировки с нуля.
Base64 для UTF-8 и Unicode — танец с TextEncoder в подробностях.
URL-safe и стандартный Base64 — когда нужны -_ вместо +/.
Попробуйте живой кодировщик Base64 — вставьте текст, посмотрите кодирование, скопируйте результат.

Сохраните эту страницу в закладки, если время от времени тянетесь к btoa() — в девяти случаях из десяти ответ на ваш вопрос уже здесь.