Buffer в Node.js: работа с бинарными данными
Содержание
О проверке кода. Примеры не запускались — в отличие от материалов с бейджем. Код дан по документации Node.js. Говорим прямо, а не ставим бейдж «проверено».
Buffer — самый «нодовый» объект: в браузере его нет, а в Node он повсюду, где данные не текст. Разберём, зачем он и где расставлены грабли.
Зачем нужен Buffer
JavaScript-строки отлично хранят текст, но плохо — сырые байты. Картинка, аудио, зашифрованные данные, сетевой пакет — это последовательности байтов, которые не являются валидным текстом ни в какой кодировке. Попытка держать их в строке портит данные.
Buffer — это участок памяти фиксированного размера для сырых байтов. Его отдают все бинарные API Node: чтение файла без кодировки, сетевой сокет, crypto, zlib.
import { readFile } from 'node:fs/promises';
const bytes = await readFile('image.png'); // Buffer — сырые байты картинки
const text = await readFile('config.txt', 'utf8'); // строка — потому что указали кодировку
Создание буфера
Buffer.from('привет', 'utf8'); // из строки
Buffer.from([0x68, 0x69]); // из массива байтов
Buffer.alloc(10); // 10 обнулённых байтов
Buffer.allocUnsafe(10); // 10 байтов БЕЗ обнуления
Здесь живёт единственная опасная развилка — alloc против allocUnsafe.
Buffer.alloc(n)
Выбор по умолчанию
- Обнуляет память перед выдачей
- Предсказуемое содержимое
- Чуть медленнее
- Безопасно отдавать наружу
Buffer.allocUnsafe(n)
Только с полной перезаписью
- Не обнуляет — там старые данные процесса
- Быстрее
- Отдать не заполнив = утечка чужой памяти
- Оправдан, только если сразу пишете весь буфер
allocUnsafe быстрее, потому что пропускает обнуление — но в выданной памяти лежат остатки предыдущих данных процесса: обрывки паролей, чужих запросов, чего угодно. Отдать такой буфер клиенту, не перезаписав целиком, — значит отправить ему случайные фрагменты чужой памяти. Пока не упрётесь в производительность, берите alloc.
Кодировки: байты и текст
Буфер превращают в строку и обратно, указывая кодировку:
const buf = Buffer.from('привет', 'utf8');
buf.length; // 12 — БАЙТ, не символов
buf.toString('utf8'); // "привет"
buf.toString('hex'); // "d0bfd180d0b8d0b2d0b5d182"
buf.toString('base64'); // "0L/RgNC40LLQtdGC"
Главная ловушка — длина. buf.length — это байты, а "привет".length — символы. Для кириллицы разница в два раза, для эмодзи — до четырёх. Отсюда классическая ошибка: считать размер текста по числу символов или наоборот.
Полезные кодировки: utf8 (текст), hex (для отладки и хешей), base64 (передача бинарного как текста, например в JSON или data-URL).
Склейка: только concat
// ❌ через строку — многобайтовый символ может порваться
let result = '';
for (const chunk of chunks) result += chunk; // риск испортить кириллицу
// ✅ собираем байты, декодируем один раз
const result = Buffer.concat(chunks).toString('utf8');
Почему нельзя через строку: если кириллическая буква (два байта) окажется на границе двух буферов, преобразование каждого куска в строку по отдельности разорвёт её пополам, и вы получите «кракозябру». Buffer.concat склеивает байты, а декодирование происходит один раз в конце, когда символы точно целы.
Это ровно причина, по которой тело HTTP-запроса собирают через concat, а не строкой.
Чтение чисел из буфера
Буфер умеет то, чего не умеет обычный массив, — читать числа разного размера:
const buf = Buffer.from([0x00, 0x01, 0x00, 0x00]);
buf.readUInt8(0); // 0 — один байт
buf.readUInt16BE(0); // 1 — два байта, big-endian
buf.readUInt32LE(0); // 256 — четыре байта, little-endian
BE и LE — порядок байтов (big-endian / little-endian). Это нужно при разборе бинарных форматов и сетевых протоколов, где число занимает несколько байтов в определённом порядке. Для прикладного кода — редко, но когда встречается, без этого никак.
Buffer и производительность
Буфер работает вне обычной кучи V8 (в разделе external из process.memoryUsage()), поэтому большие буферы не давят на сборщик мусора так, как большие строки. Но у них есть предел — buffer.constants.MAX_LENGTH, и очень большие данные всё равно обрабатывают потоком, а не целиком в буфере.
Смежные темы: модуль fs — где буферы возникают; потоки — обработка бинарного по кускам; crypto — хеши возвращают буферы. Полный список — в справочнике по Node.js.
Частые вопросы
Зачем нужен Buffer, если есть строки?
Чем Buffer.alloc отличается от Buffer.allocUnsafe?
alloc обнуляет память перед выдачей — безопасно. allocUnsafe быстрее, но не обнуляет: в буфере остаются старые данные процесса. Отдавать такой буфер наружу не заполнив — утечка чужой памяти. По умолчанию берите alloc.Почему длина буфера не равна длине строки?
Buffer.from('привет').length вернёт 12, а строка — 6 символов.Buffer — это массив?
Как склеить несколько буферов?
Buffer.concat([buf1, buf2]). Складывать через строку нельзя: многобайтовый символ может разорваться между буферами и испортиться. Собирайте байты, декодируйте один раз в конце.