Формула
Для входных данных длиной n байт Base64 с дополнением (по умолчанию в RFC 4648) имеет длину ровно:
length = 4 × ceil(n / 3)
Без дополнения, как в Base64URL для JWT:
length = ceil(4n / 3)
Они различаются на число знаков =: 0, 1 или 2 в зависимости от n mod 3. Откуда они берутся, объясняет статья про дополнение.
В обратную сторону: строка Base64 с дополнением длиной L декодируется в:
bytes = 3 × L / 4 − (number of trailing "=")
Так что TWFu декодируется в 3 байта, TWE= в 2, а TQ== в 1. Перед подсчётом удалите пробелы и переносы строк. Калькулятор размера Base64 считает в обе стороны, с учётом переносов строк и префиксов data URI.
В коде:
import base64, math, os
for n in [1, 2, 3, 100, 1000]:
data = os.urandom(n)
assert len(base64.b64encode(data)) == 4 * math.ceil(n / 3)
const b64Len = (n) => 4 * Math.ceil(n / 3);
const rawLen = (s) => (s.length * 3) / 4 - (s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0);
b64Len(1000); // 1336
rawLen("TWE="); // 2
Откуда берутся 33%
В байте 8 бит. Символ Base64 несёт 6 бит данных, потому что он один из 64 = 2⁶ символов, но хранится как 8-битный байт ASCII. Значит, каждый выходной байт содержит 6 полезных бит, и вывод должен быть в 8/6 = 4/3 раза больше входа. Это 33,3% сверху, и это жёсткий предел: ни одна реализация Base64 не сделает лучше, не отказавшись от алфавита. (Как перегруппировываются биты, пошагово показано в статье как работает Base64.)
На маленьких входных данных дополнение ухудшает соотношение, потому что вывод всегда идёт блоками по 4. Один байт превращается в 4 символа, то есть размер растёт на 300%. Уже на нескольких сотнях байтов дополнение становится пренебрежимо малым, и соотношение устанавливается на 4/3.
Есть и более плотные текстовые кодировки. Base85 хранит 4 байта в 5 символах (рост на 25%), у Base91 ещё чуть меньше. Их поддерживают реже, а в их алфавитах есть символы, которые кое-где создают проблемы. Сравнение есть в статье Base64, hex, Base32, Base58 и Base85.
Переносы строк: MIME, PEM и утилиты командной строки
Некоторые форматы разбивают Base64 на строки, и окончания строк тоже входят в размер.
| Формат | Длина строки | Окончание строки | Итоговый рост |
|---|---|---|---|
| Обычный Base64 (RFC 4648) | без переносов | нет | 33,3% |
| Почта MIME (RFC 2045) | 76 | CRLF (2 байта) | 36,8% |
GNU base64, Python encodebytes | 76 | LF (1 байт) | 35,1% |
| Сертификаты и ключи PEM | 64 | LF | 35,4% |
Цифра для MIME получается из 4/3 × 78/76: каждые 76 символов Base64 приносят ещё 2 байта CRLF. При L = 4 × ceil(n / 3) размер с CRLF после каждой строки, включая последнюю, равен:
mime_length = L + 2 × ceil(L / 76)
Каждая полная строка из 76 символов содержит ровно 57 входных байт, поэтому кодировщики MIME обрабатывают данные кусками по 57 байт. О почте подробнее написано в статье Base64 в почте, а вывод с переносами выдаёт кодировщик MIME Base64.
Таблица размеров
Посчитано по формулам выше и сверено с модулем base64 из Python.
| Вход | Base64 с дополнением | Без дополнения | MIME (76 + CRLF) | Рост (с дополнением) |
|---|---|---|---|---|
| 1 байт | 4 | 2 | 6 | 300% |
| 2 байта | 4 | 3 | 6 | 100% |
| 3 байта | 4 | 4 | 6 | 33,3% |
| 10 байт | 16 | 14 | 18 | 60% |
| 57 байт | 76 | 76 | 78 | 33,3% |
| 100 байт | 136 | 134 | 140 | 36% |
| 1000 байт | 1336 | 1334 | 1372 | 33,6% |
| 1 КиБ (1024) | 1368 | 1366 | 1404 | 33,6% |
| 1 МБ (10⁶) | 1 333 336 | 1 333 334 | 1 368 424 | 33,3% |
| 1 МиБ (1 048 576) | 1 398 104 | 1 398 102 | 1 434 898 | 33,3% |
| 10 МиБ | 13 981 016 | 13 981 014 | 14 348 938 | 33,3% |
| 25 МБ (25 × 10⁶) | 33 333 336 | 33 333 334 | 34 210 530 | 33,3% |
Последняя строка важна для почты. Ограничения размера в SMTP (расширение SIZE, RFC 1870) считают письмо в том виде, в каком оно передаётся, то есть вместе с кодированием, поэтому файл на 25 МБ при передаче занимает около 34,2 МБ и не пройдёт через лимит сервера в 25 МБ. Некоторые почтовые сервисы описывают свои лимиты по размеру вложения, так что уточните, как считает ваш.
Что даёт сжатие
Сжатие и Base64 сочетаются двумя способами, и порядок здесь важен.
Сжимайте до кодирования, когда контролируете обе стороны. Base64 от сжатых данных занимает те же 4/3, но от меньшего числа. Для текста лицензии GPL-3 (35 149 байт) мы измерили размеры через zlib в Node.js при уровне gzip 9:
| Цепочка | Размер |
|---|---|
| Исходный текст | 35 149 |
| Только Base64 | 46 868 |
| Base64, затем gzip | 17 465 |
| gzip, затем Base64 | 16 124 |
| Только gzip | 12 091 |
Инструмент Gzip + Base64 выполняет цепочку «gzip, затем Base64» в браузере.
Сжатие при передаче возвращает большую часть потерь для двоичных данных. Если строка Base64 идёт внутри HTTP-ответа, сжатого gzip или Brotli, компрессор ужимает 6-битные символы почти обратно до 6 бит каждый. Для 1 МиБ случайных байтов Base64 вырос до 1 398 104 символов; после gzip получилось 1 053 023 байта (на 0,4% больше исходных данных), а Brotli довёл размер до 1 048 805. Те же данные с переносами строк MIME после gzip заняли 1 088 689 байт, так что CRLF стоят около 3,4% даже после сжатия.
С текстом иначе. Base64 разрушает повторы, на которые опираются компрессоры, поэтому текст в Base64 сжимается гораздо хуже, чем сам текст: 17 465 против 12 091 байта в таблице выше. В статье о производительности data URI есть замеры для PNG, JPEG, WOFF2 и SVG.
Лимиты URL, заголовков и cookie
Base64 раздувает всё, что вы кладёте в URL, заголовок или cookie, а у всех трёх есть ограничения.
- URL. RFC 9110 рекомендует отправителям и получателям поддерживать URI длиной не меньше 8000 октетов. Многие серверы по умолчанию останавливаются примерно там же: nginx выделяет под строку запроса буферы заголовков по 8 КБ, а
LimitRequestLineв Apache по умолчанию равен 8190 байтам. Более длинные URL обычно падают с414 URI Too Long. С Base64URL это место примерно под 6000 байт данных за вычетом остального URL. - Заголовки. Те же серверные буферы действуют на каждую строку заголовка, а это важно для длинных JWT в заголовках
Authorization: Bearer ...и для больших учётных данных Basic Auth. - Cookie. RFC 6265 просит браузеры поддерживать не меньше 4096 байт на cookie, включая имя и атрибуты. Это примерно 3 КБ данных после кодирования в Base64.
Если полезная нагрузка в Base64 приближается к одному из этих лимитов, передавайте её в теле запроса или храните на сервере и передавайте идентификатор.
Введите число байтов или вставьте строку, чтобы увидеть размер до и после кодирования, с переносами строк и без.
Вопросы и ответы
Насколько Base64 больше исходного файла?
Примерно на 33,3% для всего, что длиннее нескольких байтов. С переносами строк MIME (76 символов плюс CRLF) рост составляет примерно 36,8%.
Как по строке Base64 посчитать исходный размер?
Возьмите длину без пробельных символов, умножьте на 3/4 и вычтите число знаков = в конце. Например, TWE= даёт 4 × 3/4 − 1 = 2 байта.
Почему 1 байт превращается в 4 символа?
С дополнением Base64 всегда выдаёт целые блоки по 4 символа. Один байт занимает 2 символа, а остальные 2 заняты знаками =, поэтому у крошечных входных данных относительный рост размера гораздо больше.
Можно ли сделать Base64 компактнее?
Сожмите данные перед кодированием, например через gzip, и отбросьте дополнение, если получатель это допускает. У самого Base64 соотношение фиксированное; есть и более плотные текстовые кодировки вроде Base85, но их поддерживают реже.