Кодирование Base64 в JavaScript/браузере: полное руководство
У вас есть что-то, что должно отправиться в путь, а дорога широка только для чистого ASCII. Это может быть изображение, которому место внутри JSON-ответа, конфигурационный объект, который обязан поехать в URL, токен, у которого три сегмента из точек и букв, файл, который API требует прислать Base64-строкой внутри JSON-тела. Base64 - платный пост именно для такой ситуации, и домашняя страница этого сайта уже прошла по формату - четыре печатаемых символа за каждые три байта, со знаками =, чтобы дописать группу, - поэтому вот одно число, которое стоит держать в голове, пока вы читаете: кодирование - это растущее направление. Каждые три сданных байта возвращаются четырьмя символами, налог на размер примерно 33 процента, собираемый полосой пропускания, хранилищем и памятью. Используйте Base64, когда канал требует печатаемый текст, и точно знайте, что стоит вам этот налог.
Утешительная новость в том, что браузер всегда умел делать эту работу без единого пакета. btoa() поставляется с начала 2000-х, TextEncoder десять лет назад превращал ваш настоящий Unicode-текст в честные байты, а в волне Baseline 2025 платформа наконец добавила Uint8Array.toBase64(), который кодирует массивы байтов напрямую, с опцией для URL-безопасного алфавита. Эта статья - карта решений: какой инструмент для какой работы, где прячутся острые края (все они ведут к одной и той же границе) и конкретные рецепты для мест, где от вас действительно запросят произвести Base64.
Выбор кодировщика
Единого «того самого» кодировщика больше нет, и именно замах на неподходящий рождает классические баги. Таблица ниже - всё дерево решений:
| Ситуация | Берите |
|---|---|
| Простой ASCII-текст, разовое значение | btoa(text) |
| Настоящий текст с акцентами, эмодзи, CJK | new TextEncoder().encode(text), затем btoa или toBase64 |
Байты уже в Uint8Array |
bytes.toBase64() в браузерах 2025 года и новее, чанкованный мост btoa - везде остальном |
| URL, JWT, имена файлов | toBase64({ alphabet: 'base64url', omitPadding: true }) |
| Старые браузеры или общий код | js-base64 либо классический рецепт TextEncoder + btoa |
Паттерн под таблицей: btoa() читает только однобайтовые символы, поэтому всё, что не ASCII, должно сначала стать массивом байтов, и именно этот массив байтов - то, вокруг чего строились современные API. Держите в голове «текст становится байтами, байты становятся Base64», и каждый рецепт в этой статье - те же два шага с разными именами.
btoa и граница Latin1
btoa(stringToEncode) - бинарная строка в ASCII-строку - это оригинальный кодировщик, доступный в каждом браузере, который что-то значит (Chrome 4, Firefox 1, Safari 3, IE 10 и выше, все worker-области, и Node начиная с версии 16). У его договора одна оговорка, и именно на ней всё ломается: каждый символ на входе должен иметь код от 0 до 255. Функция читает коды, а не UTF-8 байты, поэтому «é» (код 233) проскакивает мимо, а «你» (код 20320) бросает DOMException с именем InvalidCharacterError, прежде чем закодирован хотя бы один символ. Граница - это не «ASCII», это не «Unicode», это ровно 256, и она включает управляющие символы внизу - кодирование NUL-байта законно и осмысленно, и это одна из причин, по которой функция вообще существует.
Полное поведение, построчно:
| Вход | Результат |
|---|---|
"Hello, World!" |
"SGVsbG8sIFdvcmxkIQ==" - классический случай |
"" (пустая строка) |
"" - ничего не вошло, ничего не вышло |
"\u0000" (NUL) |
"AA==" - управляющие символы - граждане первого сорта |
"a\u00e9z" (é, код 233) |
"Yel6" - проходит весь диапазон Latin1 |
"\u0100" (код 256) |
бросает InvalidCharacterError - один шаг за границей |
"h\u4f60" (你, код 20320) |
бросает InvalidCharacterError - и каждый эмодзи тоже, потому что все они далеко выше 255 |
Две практические заметки. Сообщение об ошибке различается в движках - Firefox говорит «String contains an invalid character», Chrome говорит, что строка «contains characters outside of the Latin1 range», - поэтому в защитном коде ловите по имени исключения. И сбой происходит на первом провинившемся символе, а не в конце: btoa не кодирует половину строки и не извиняется. Когда вам на самом деле нужно Latin1-поведение (кодирование байтовой строки, которую нарочно собрали из кодов 0-255), функция делает ровно то, что вы просили, и таблица выше - весь её характер.
Мост байтов
И вопрос становится таким: как реальные данные - UTF-8 байты вашего текста, содержимое файла, вывод canvas - попадают на вход btoa()? Ответ - «мост байтов»: JavaScript-строка, в которой каждый символ хранит значение одного байта, тот же трюк, что производят декодеры и который btoa понимает нативно. Наивная версия - цикл:
function bytesToBase64 (bytes) {
let binary = '';
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
Верно, но конкатенация строк в цикле медленна для больших файлов, а популярное сокращение - String.fromCharCode.apply(null, bytes), которое скармливает весь массив аргументами в одном вызове, - упирается в жёсткий обрыв. У вызовов функций есть лимит на число аргументов, и он достигается задолго до вашего первого мегабайта:
const big = new Uint8Array(1000000);
btoa(String.fromCharCode.apply(null, big));
// RangeError в Firefox: «too many arguments provided for a function call»
// RangeError в Chrome: «Maximum call stack size exceeded»
Фикс, который спас больше функций загрузки файлов, чем любое другое единичное изменение, - пересекать мост кусками, по несколько тысяч символов за раз, и склеить результаты:
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(''));
}
Каждый кусок достаточно мал для безопасного apply, subarray даёт представление без копирования, а склейка порождает точно ту же бинарную строку, которую дал бы цикл. Теперь текстовая сторона медали. Для любого настоящего текста TextEncoder - UTF-8-кодировщик платформы, доступный в Firefox 18, Chrome 38, Safari 10.1 и везде с тех пор, - превращает вашу строку в честные байты, прежде чем мост берётся за работу:
const bytes = new TextEncoder().encode('hello 你好');
const base64 = bytesToBase64Chunked(bytes);
console.log(base64); // "aGVsbG8g5L2g5aW9"
Такой вывод - то, чем «hello 你好» является на самом деле на проводе: шесть ASCII-байтов плюс шесть UTF-8-байтов за два китайских иероглифа, все в одной и той же печатаемой маске. Если ваш текст не UTF-8 - а в вебе он обычно именно UTF-8, - сначала нужна другая кодировка, а значит, кодировать его где-то, где говорят на ней, обычно на сервере. TextEncoder нарочно отказывается догадываться, и он прав.
Шорткат 2025 года: Uint8Array.toBase64
Если у вас уже есть Uint8Array, мост - это крюк, потому что новая фича ECMAScript (ES2026) кодирует массив напрямую: bytes.toBase64(options). Она прибыла в Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 и Deno 2.5 - та же волна Baseline 2025, что и у её декодирующей сестры, - и принимает две опции, которые превращают её в самый многогранный кодировщик платформы. Первая - alphabet: "base64" (по умолчанию) или "base64url". Вторая - omitPadding: выставьте true, и хвостовые символы = отбрасываются, а это именно та форма, которую хочет большинство URL-дружелюбных потребителей. Передадите что-нибудь другое в опциях - получите TypeError, что есть вежливость API по поводу вашей опечатки:
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="
Эти два байта выбраны максимально невоспитанно по отношению к алфавиту: в стандартном режиме они дают + и /, так что последняя строка показывает ровно, что меняется при переключении на base64url. Производительность - тихий бонус: на свежем Firefox кодирование десяти мегабайт занимает около пяти миллисекунд с toBase64, тогда как путь через строковый мост выше занимает примерно в пятнадцать раз больше, потому что по ходу строит гигантскую промежуточную строку. В старых браузерах мост остаётся вполне пригодным для всего, что меньше нескольких мегабайт, - а чанкованная версия выше - именно та, что вам нужна, по причинам из предыдущего раздела.
URL-безопасный вывод
У Base64 есть отдельный вариант для мест, где +, / и = устраивают повреждения, и он заслуживает собственного раздела, потому что столько сломанного кода - это просто стандартный Base64, которому на пути попался URL. В строке запроса + - это пробел; в пути / - разделитель; а = в некоторых позициях просит процентного кодирования. Безопасный для URL и имён файлов алфавит из раздела 5 RFC 4648 - base64url - меняет эти два символа на - и _, а раз длина данных на приёмной стороне обычно известна, он разрешает отбросить заполнитель целиком. Вывод проходит через URLSearchParams, сегменты пути, фрагменты и имена файлов без единого процентного знака.
С API 2025 года это один объект опций:
const params = new URLSearchParams();
params.set('payload', bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(params.toString()); // «payload=-_8» - без процентного кодирования вообще
В старых браузерах преобразуйте после кодирования через btoa. Два replace и trim делают всю работу:
function toUrlBase64 (base64) {
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
console.log(toUrlBase64(btoa('hi?/x'))); // "aGk_L3g"
Три правила держат канал чистым. Выберите один алфавит на канал и держитесь его - значение, где смешаны + и -, не принадлежит ни одной семье, и ни один декодер не догадается, что вы имели в виду. Заполнитель - это договор, а не рекомендация: если вы его опускаете, получатель должен быть готов к значению без заполнителя, а если оставляете, получатель не должен спотыкаться о него (браузеры снисходительны, некоторые JSON-схемы - нет). И помните, что замена обратима и без потерь - - и _ отображаются на те же 62-ю и 63-ю позиции алфавита, которые занимают + и /, так что при выборе дружелюбной пары ничего не теряется.
Помогаем изображениям путешествовать: data URL
Старейшее и самое видимое применение Base64 в браузере - это data URL: data:, необязательный медиатип, необязательный флаг ;base64, запятая, а потом пелод. Текстовые нагрузки процентно кодируются; бинарные - изображения, шрифты, аудио - в Base64, и браузер отрисовывает их с нулём HTTP-запросов. Для файла изображения, который пользователь только что выбрал, FileReader делает кодирование за вас и отдаёт готовый URL:
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result); // "data:image/png;base64,iVBORw0KGgo..."
imageElement.src = reader.result;
};
reader.readAsDataURL(file);
Результат - готовое src, значение, которое можно сохранить в localStorage или отправить в JSON-теле. Если изображение лежит не в файле, а на canvas - скриншот, обработанное фото, сгенерированный график, - canvas.toDataURL() делает эту работу с самых ранних браузерных релизов, и он даже позволяет выбрать формат и, для форматов с потерями, качество:
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);
Три ловушки, вокруг которых стоит планировать. Первая - правило запачканного canvas: если вы нарисовали кросс-доменное изображение на canvas без разрешения CORS, любая попытка прочитать пиксели обратно - включая toDataURL, - бросает SecurityError. Фикс - загрузить изображение с crossOrigin = 'anonymous' и убедиться, что сервер шлёт правильные заголовки. Вторая - аргумент качества игнорируется для PNG и имеет смысл только для JPEG (и WebP), - частый источник вопроса «почему мой PNG больше». Третья, и самая большая: пелод примерно на 33 процента больше файла, и он сидит в странице строкой. Для изображений, которые никогда не выходят из браузера, есть бесплатная альтернатива - object URL, который оборачивает Blob, вообще его не кодируя:
const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
URL.revokeObjectURL(objectUrl); // когда он вам уже не нужен
Разделение труда, которое из этого выходит: object URL для всего, что остаётся на странице, data URL для всего, что нужно скопировать, сохранить или отправить текстом. Оба - граждане первого класса; они просто решают разные задачи.
Создание и подпись JWT
Если вы генерируете токены в браузере - для self-hosted потока аутентификации, демо или serverless фронтенда, - компактный формат JWS - это три base64url-сегмента: заголовок, пелод, подпись, заполнителя нигде. Подпись берёт на себя Web Crypto API; кодирование - ровно тот URL-безопасный вывод из двух разделов назад:
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;
Две детали важнее всей сантехники. Подпись покрывает ровно header + '.' + payload - сырые сегменты, а не JSON, - поэтому любое изменение любой из частей инвалидирует токен, и в этом весь смысл. А crypto.subtle.sign возвращает сырой ArrayBuffer, отсюда однострочная обёртка в Uint8Array перед сегментным кодировщиком. Для токенов на базе RSA поток идентичен с RS256 и парой ключей, и если вы экспортируете публичный ключ как JWK (crypto.subtle.exportKey('jwk', key)), числовые члены - n, e и для приватных ключей d, p, q, - автоматически выходят base64url без заполнителя. Оговорки по безопасности те же, что и для любого токена: заголовок alg: "none" - это просьба пропустить проверку, временные заявления (exp, nbf) должны применяться, а сервер, принимающий и HMAC, и RSA для одной и той же аудитории, открывает классическую дверь путаницы ключей. Кодируйте правильно, подписывайте правильно, проверяйте на стороне приёма.
Заголовки аутентификации
Самая простая схема аутентификации в вебе также наиболее поучительна о том, что такое Base64 и чего он не является. HTTP Basic отправляет Authorization: Basic с последующей Base64-кодировкой username:password - один вызов, мост байтов не нужен, потому что имена пользователей и пароли (надеюсь) - обычный текст:
const credentials = btoa('alice:secret123');
fetch('/api/me', {
headers: { Authorization: 'Basic ' + credentials }
});
// Authorization: Basic YWxpY2U6c2VjcmV0MTIz
И вот урок, который умещается в одну строку: Base64 - это не шифрование. Заголовок выше - в одном вызове atob от alice:secret123 - и для злоумышленника, и для всех, кто читает логи, - поэтому Basic-аутентификация приемлема только поверх HTTPS, где транспорт - настоящая защита, а Base64 - лишь оформление. Для всего, что живёт дольше одного запроса, предпочитайте токенные схемы: Bearer-токен - тоже один заголовок, но это случайное значение, чей секрет вообще не нужно носить в заголовке, и его можно отозвать. Выбор кодирования между ними тривиален - оба это btoa или обычный текст, - а вот выбор в безопасности нет, и его стоит сделать осознанно.
Файлы на входе, текст на выходе
Загрузки - это место, где 33-процентный налог называют уже в реальных деньгах, потому что файл обычно - самая большая вещь на странице. Есть две дороги, и первая - та, которую стоит брать по умолчанию: multipart-формы данных. FormData несёт файл сырыми байтами в стандартном теле, браузер делает фрейминг, и Base64 здесь нет нигде - ни налога на размер, ни промежуточной строки, и байты стекаются на сервер по мере чтения:
const form = new FormData();
form.append('upload', file);
await fetch('/api/upload', { method: 'POST', body: form });
Вторая дорога - для API, которые настоятельно требуют JSON-тело с файлом в виде строки - некоторые serverless-функции, некоторые мобильные бэкенды, некоторые легаси-сервисы. Там кодирование - одна строка на файл, и цена ровно та, что обещает налог: файл в 5 мегабайт становится строкой в 6.7 мегабайта, которая затем сериализуется в JSON, который затем отправляется. Для фото - нормально, для видео - больно:
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
});
Для больших файлов на той дороге не стройте одну гигантскую строку в одном вызове - стройте слайсами, где каждый слайс - кратное трём число байтов. Именно это выравнивание делает трюк законным: кратное трём число байтов кодируется в чистое кратное четырём число символов без заполнителя, так что независимо закодированные слайсы склеиваются ровно в кодирование всего файла, и заполнитель несёт только последний слайс:
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('');
}
Идея того же выравнивания объясняет, почему никогда не стоит резать Base64-строку в произвольной точке и ожидать, что куски декодируются сами по себе - трёхбайтовая группа - это атом, и разрез посередине оставляет висеть фрагмент. Скачивания - зеркальное отражение: сгенерированный маленький файл может уйти через data URL в ссылке для скачивания, но для всего существенного Blob плюс object URL - здоровый путь, потому что браузеру в принципе не приходится нести всю пелод строкой.
Сохранение и раздача состояния
Ещё два чисто текстовых канала, где Base64 делает реальную работу. Первый - хранилище: localStorage и sessionStorage держат строки, поэтому структурированные или бинарные данные кодируются прежде, чем попасть туда. Круговое путешествие - одно кодирование и одно декодирование, и обе стороны стоит посмотреть вместе, потому что баг хранилища почти всегда - несовпадение кодировок между ними:
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));
Но посчитайте бюджет как следует: происхождение получает примерно 5 мегабайт localStorage, ваша сохранённая строка на 33 процентов жирнее данных, и пока страница открыта, строка ещё живёт в памяти как UTF-16 - снова вдвое больше по длине. Ассет в 3 мегабайта - это 4 мегабайта хранилища и 8 мегабайт памяти, и так «маленькая» фича превращается в ошибку квоты. Второй канал - сам URL: ссылки для раздачи, глубокие ссылки и OAuth-состояние хотят структурированные данные там, где они переживут копирование. Рецепт - компактное состояние, JSON, затем base64url без заполнителя, так что значение не нуждается ни в каком процентном кодировании, - и держите весь URL меньше пары тысяч символов, потому что именно там старые клиенты, прокси и инструменты логирования начинают нервничать.
Почта и MIME
Base64 старше веба, и его родная стихия - почта. MIME-вложения с Content-Transfer-Encoding: base64 - это то, как бинарный файл едет внутри текстового протокола, и из старого лимита в 76 символов на строку в формате сообщений следует традиция, которую стоит знать: заворачивайте закодированное тело по 76 символов в строку. Браузер не умеет слать SMTP, но он делает две почтовые работы - собирает MIME-тела, которые отправит бэкенд-релей, и отображает вложения входящих сообщений, - и обе касаются кодирования. Само заворачивание - функция из двух строк, и порядок операций важен: сначала кодируем, потом заворачиваем, потому что btoa не бросит исключение на переводе строки во входе - он закодирует перенос как байт пелода, и ваши переводы строк окажутся внутри вывода:
function wrapForMime (base64, width) {
const w = width || 76;
return base64.match(new RegExp('.{1,' + w + '}', 'g')).join('\r\n');
}
Приёмная сторона - бесплатная: atob пропускает ASCII-пробельные символы как часть стандартного поведения, так что завёрнутое MIME-тело декодируется ровно так, как пришло, переводами строк и всем остальным, без шага разворачивания. Если вы строите веб-почтовый клиент или выборщик вложений, то эта единственная асимметрия - кодировщик обязан производить чистые строки, декодер всё равно, - и есть вся MIME-история в одном предложении.
Когда пора браться за библиотеку
С нативными инструментами выше библиотека редко нужна, и честный совет такой: по умолчанию берите платформу, а пакет добавляйте только когда реальное требование указывает на один. Три, которые действительно встречаются в кодовых базах:
js-base64 (npm install js-base64) - универсальная: маленький чисто-JavaScript транскодер, который считает UTF-8 строки гражданами первого класса - Base64.encode на CJK-строке делает UTF-8 танец за вас, - и, что полезно для декодирования не меньше, чем для кодирования, принимает оба алфавита в decode и поставляет проверку isValid. Это правильный ответ, когда вы целитесь в браузеры, где API 2025 года отсутствуют, и хотите, чтобы один импорт покрыл и строки, и байты:
import { Base64 } from 'js-base64';
const encoded = Base64.encode('小飼弾'); // «5bCP6aO85by+» - UTF-8 обрабатывается за вас
const decoded = Base64.decode('5bCP6aO85by-'); // читает и стандартный, и URL-safe вариант
const valid = Base64.isValid(encoded); // true
base64-js - байто-ориентированная: fromByteArray и toByteArray на Uint8Array, без зависимостей, рабочая лошадка старой экосистемы browserify и по-прежнему неплохой выбор, когда ваш код живёт в типизированных массивах и вы хотите, чтобы кодирование было чистой функцией байтов. А если ваша причина хотеть библиотеку - «мне нравится API 2025 года, но я не могу требовать браузеры 2025 года», - ответ вообще не в Base64-пакете, а в полифилле: core-js (и Babel-пресет, который втаскивает его) реализует Uint8Array.fromBase64 и друзей, так что вы пишете код в новом стиле один раз и даёте шиму заполнить пробел на старых движках. Выбирайте по ограничению - старые браузеры, удобство со строками или чистота байтов, - а не по привычке.
Ловушки, от которых разработчики теряли часы
- Вызов
btoaна строке с символом выше кода 255. Она бросает исключение, а не калечит, и останавливается на первом нарушителе. Фикс всегда один: сначалаTextEncoder, потом мост. - Обрыв
fromCharCode.applyна больших массивах. Миллион аргументов - этоRangeErrorв обоих главных движках. Чанкуйте мост или переходите наtoBase64. - Забывание налога на размер там, где больнее всего: в хранилище. Файл в
localStorageна 33 процента больше самого файла, а квота - на происхождение, общая со всем остальным, что сохраняет ваше приложение. - Стандартный Base64 встречает строку запроса.
+приезжает пробелом,/ломает путь, и в баг-репортах пишут «API капризный». URL-безопасный вывод, без заполнителя, и целый жанр багов исчезает. - Несогласованный заполнитель между сервисами. Один шлюз держит
=, другой срезает, третий добавляет обратно. Получатель должен быть готов к обеим формам, а договор должен говорить, какая из них каноническая. - Отношение к Base64 как к замку. Это формат сериализации, один вызов функции от открытого текста, и «закодировано в Base64» в ревью безопасности - это находка, а не мера контроля.
- Бинарные строки как модель памяти. Декодированный или закодированный мегабайт едет в UTF-16 на двух мегабайтах;
Uint8Arrayдержит его на одном. Для больших нагрузок держите байты в типизированных массивах от начала до конца. - Двойное кодирование. Значение, которое уже было Base64, кодируют ещё раз, и потребитель декодирует один раз - и вместо данных получает строку из букв. Если сомневаетесь, проверьте до обёртки - строка, которая уже в алфавите и с корректным заполнителем, - это запах.
- Доверие пелоду JWT потому, что он чисто декодировался. Декодируемость - это не подлинность. Проверьте подпись правильным ключом и правильным алгоритмом, прежде чем читать хоть одно заявление.
Производительность: сколько стоит миллион байтов
Base64 в браузере дёшев там, где раньше был дорог, и в бюджете теперь три строки вместо одной. CPU: на свежем Firefox Uint8Array.toBase64 кодирует десять мегабайт за примерно пять миллисекунд, тогда как чанкованный мост btoa занимает примерно в пятнадцать раз больше - не потому что btoa медленный, а потому что мост по дороге строит гигантскую промежуточную строку. Если ваш бюджет кодирования - в миллисекундах, используйте нативный метод; если вы кодируете конфигурационный объект в 2 килобайта, оба варианта ниже порога восприятия. Полоса пропускания: это постоянный налог - каждый закодированный байт стоит 1.33 байта на проводе, плюс всё, что добавляет фрейминг транспорта. Измеряйте передачу, прежде чем «оптимизировать» кодирование. Память: закодированная строка - самое большое временное выделение, которое вы сделаете, и для файла в 5 мегабайт это строка в 6.7 мегабайта, или около 13.4 мегабайт UTF-16 памяти, пока страница её держит. Практические последствия вытекают из арифметики: режьте большие кодирования, чтобы ни одна строка не стала гигантской, отпускайте промежуточные байты, как только строка существует, предпочитайте object URL и multipart, когда байтам и так не нужно было быть печатаемыми, и переносите мультимегабайтную работу в Web Worker, если главный поток должен плавно продолжать прокрутку. Формату почти четыре десятилетия; платформа наконец догнала его.
Как браузеры научились кодировать
У кодировщика есть история, и она объясняет реликвии, которые вы унаследуете. btoa - «бинарное в ASCII», название буквальное, а atob - те же слова в обратном порядке, - записали в HTML-спецификацию в 2011 году, вывернув наизнанку браузеры, которые уже поставляли её: Firefox с 2004 года, Safari 3, Chrome 4. Internet Explorer, что характерно, пропустил обе функции до версии 10 в 2012 году, и этого единственного отсутствия хватило, чтобы декада JavaScript наполнилась самописными таблицами Base64 и одним особенным заклинанием для Unicode: btoa(unescape(encodeURIComponent(str))). Оно работало - encodeURIComponent производит процентно-экранированный UTF-8, а unescape превращал это в байтовую строку, - но оно стояло на unescape(), той из пары, которую язык объявил устаревшей, и оно продержалось в браузерном коде годы чистой инерцией. Принципиальный фикс пришёл со стандартом Encoding: TextEncoder и TextDecoder, в Firefox 18 (2013), Chrome 38 (2014), Safari 10.1 (2017) и ни в одной версии IE, - ещё одна IE-брешь, ещё одна декада обходных путей. Node.js рассказывает серверную половину истории: у него с первого дня был Buffer с Base64, но atob и btoa как глобалы - только с версии 16 в 2021 году, до этого нагрузку держали два мелких npm-шима. А потом, по ходу конца 2024-го и 2025-го, сам язык поставил Base64 - Uint8Array.toBase64 и друзей в Firefox 133 (ноябрь 2024), Safari 18.2 (декабрь 2024), Chrome 140 (сентябрь 2025) и Node 25 (октябрь 2025), - и фича была помечена Baseline 2025 - тот самый набор, который платформа двадцать лет приближала вспомогательными функциями, теперь стандартный. Весёлые факты в конце статьи в основном о том, сколько времени на приезд брала каждая деталь.
Знали ли вы?
- Имена функций - это фраза:
btoa- это «бинарное в ASCII», аatob- «ASCII в бинарное». Направление - в самом имени, поэтому эта пара само-документируется с 2000-х. - Самая кодируемая строка в истории вычислительной техники - скорее всего «hello»:
btoa('hello')даётaGVsbG8=, вывод каждого туториала, тестового набора и интервью-маркерборда на планете. - У каждой корректной Base64-строки длина кратна четырём, заполнитель включён. Символы
=- это отпечаток пальцев: один из них значит, что в последней группе было два байта, два - что один. - Перенос строк по 76 символов в MIME и в большинстве командно-строчных инструментов - наследие эпохи почты, когда длину строки лимитировал формат сообщений. Число пережило три десятилетия ускорения всего.
- «Data URI» - вышедшее из употребления название. WHATWG переименовал его в «data URL» во время великой гармонизации URI в URL, поэтому спецификации, блоги и имена пакетов пишут по-разному в одном абзаце.
btoa('')возвращает'': пустой вход даёт пустой вывод, без заполнителя, без особой ситуации - единственная Base64-строка с нулём символов (её длина, 0, всё равно кратна четырём).- Элемент canvas превратит фото в data URL через
toDataURL- возможность, которая существует с IE 9, Firefox 2 и Safari 4, то есть старше большей части веб-платформы, которую мы считаем «современной», - и вернёт его обратно с тегом<img>иFileReader. - WebSocket-рукопожатие кодирует
SHA-1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)в Base64, и GUID - фиксированная константа в RFC, выбранная ровно так, чтобы ни один обычный HTTP-сервер не смог случайно завершить рукопожатие.
Куда идти дальше
Всё мастерство кодирования в браузере умещается на одной странице: btoa для простых однобайтовых случаев, ради которых он и появился; TextEncoder плюс чанкованный мост для настоящего текста и файлов в любом браузере; Uint8Array.toBase64 с его опциями алфавита и заполнителя для современного прямого пути; и URL-безопасный вариант, с заполнителем или без, для всего, что будет жить в URL. Остальное - суждение: знайте 33-процентный налог до того, как его заплатить, держите байты в типизированных массивах, пока они большие, сначала кодируйте, потом заворачивайте, и никогда не называйте формат сериализации замком. Когда канал может нести сырые байты, берите байты - Base64 для дорог, которые пускают только печатаемый текст, и теперь вы точно знаете, как оплатить проезд.
Другая половина пути - принять одну из таких строк и вытащить из неё обратно байты, текст и смысл - подробно разобрана в сопутствующем руководстве по декодированию Base64 в JavaScript, ссылка ниже.
Последнее обновление: 2026-09-08
Связанная статья: Декодирование Base64 в JavaScript/браузере: полное руководство