Що таке buffer в Node.js?
Buffer - це масив байтів фіксованого розміру в Node.js, що зберігається поза купою V8 і використовується для роботи з бінарними даними: файлами, потоками, мережевими пакетами.
Теорія
TL;DR
- Buffer схожий на піднос з пронумерованими осередками: кожен тримає один байт (0-255), доступ прямий за індексом
- Головна різниця: Buffer зберігає сирі байти, рядки JavaScript кодують текст як UTF-16 (мінімум 2 байти на символ)
Buffer.alloc(n)дає n нульових байтів;Buffer.allocUnsafe(n)пропускає обнулення для швидкості, але може розкрити вміст старої пам'яті- Buffer для нетекстових даних (зображення, crypto-ключі, TCP-пакети); рядки для читабельного тексту
- Зріз Buffer не копіює пам'ять: він повертає посилання на ті самі байти
Швидкий приклад
// Створюємо, пишемо байти, читаємо як рядок
const buf = Buffer.alloc(5); // 5 нульових осередків
buf[0] = 0x48; // 'H'
buf[1] = 0x65; // 'e'
buf[2] = 0x6c; // 'l'
buf[3] = 0x6c; // 'l'
buf[4] = 0x6f; // 'o'
console.log(buf.toString('utf8')); // "Hello"
console.log(buf); // <Buffer 48 65 6c 6c 6f>
// slice повертає посилання, не копію
const view = buf.slice(0, 3);
console.log(view.toString()); // "Hel"buf[0] = 0x48 записує байт для 'H' безпосередньо в пам'ять. toString('utf8') декодує ці байти назад у текст. Зріз ділить пам'ять з оригінальним buf, нового виділення не відбувається.
Головна відмінність від рядків
Рядки JavaScript - це UTF-16: кожен символ займає мінімум 2 байти і живе всередині купи V8 під управлінням збирача сміття. Buffer зберігає 8-бітні цілі числа (0-255) у пам'яті, виділеній через libuv, поза купою V8. Без тиску на GC, без накладних витрат кодування. Коли читаєш JPEG з диска, потрібні байти, не символи. Саме тут Buffer на своєму місці.
Коли використовувати
- Файловий I/O з бінарними форматами (зображення, PDF, zip): Buffer
- Файловий I/O з текстом (JSON, CSV, логи): рядок
- TCP/UDP сокети, сирі мережеві фрейми: Buffer
- Тіло HTTP-відповіді, яке вже є текстом: рядок
- Криптографічні операції (ключі, хеші, шифротекст): завжди Buffer, рядки можуть пошкодити байти через кодування
- Стримінг великих файлів: обробляй чанками Buffer, декодуй у рядок тільки перед відображенням
Buffer.allocUnsafeпідходить, якщо одразу перезаписуєш кожен байт; інакше використовуйBuffer.alloc
Як це працює всередині
Node.js виділяє пам'ять для Buffer через пули сирої C-пам'яті libuv, а не через V8. Збирач сміття не чіпає цю пам'ять під час звичайного виконання, що дозволяє уникнути пауз GC, коли тримаєш гігабайти відео або аудіо. V8 представляє Buffer як об'єкти, схожі на Uint8Array, з методами, підтриманими C++. Наприклад, buf.write() викликає uv_buf_init під капотом. Починаючи з Node.js 4, Buffer - це підклас Uint8Array, тому методи TypedArray працюють на ньому напряму.
Зріз (slice) - це zero-copy: buf.slice(0, 5) повертає зміщення покажчика в той самий блок пам'яті, без нового виділення. Швидко, але зміни в зрізі зачіпають оригінал.
Типові помилки
Помилка 1: використання Buffer.allocUnsafe без перезапису всіх байтів
const buf = Buffer.allocUnsafe(10);
buf.write('hi');
// Байти на позиціях 2-9 можуть містити стару пам'ять процесу
console.log(buf); // <Buffer 68 69 XX XX XX XX XX XX XX XX>allocUnsafe пропускає обнулення заради продуктивності. Якщо відправиш цей Buffer по мережі або запишеш на диск, незаписані байти розкривають те, що було в тій пам'яті раніше. На практиці це найпоширніша причина тихого пошкодження даних у бінарних пайплайнах. Я бачив як команди витрачали пів дня на дебаг пошкоджених зображень, поки не відстежили проблему до allocUnsafe з незаписаними хвостовими байтами. Рішення: використовуй Buffer.alloc(10) або одразу після allocUnsafe викликай buf.fill(0).
Помилка 2: припущення що slice копіює дані
const big = Buffer.alloc(1_000_000);
const small = big.slice(0, 10);
small[0] = 0xff;
console.log(big[0]); // 255 - ти мутував big тежЗріз ділить пам'ять з оригіналом. Якщо потрібна незалежна копія, використовуй Buffer.from(small).
Помилка 3: зріз посеред emoji при роботі з Unicode
const buf = Buffer.from('😊👍', 'utf8');
console.log(buf.length); // 8 байтів (4 на emoji в UTF-8)
const broken = buf.slice(0, 3); // ріже першу emoji посередині
console.log(broken.toString('utf8')); // пошкоджений вивід
const correct = buf.toString('utf8', 0, 4); // повна перша emoji
console.log(correct); // "😊"Buffer працює на рівні байтів, не символів. Багатобайтові Unicode-символи пошкоджуються, якщо зміщення не збігається з межею символу.
Помилка 4: забути передати кодування 'hex'
// Неправильно: рядок hex читається як UTF-8 текст
Buffer.from('48656c6c6f');
// Правильно
Buffer.from('48656c6c6f', 'hex'); // <Buffer 48 65 6c 6c 6f> = "Hello"Кодування за замовчуванням - 'utf8'. Завжди передавай другий аргумент при роботі з hex або base64.
Помилка 5: сприймати Buffer як незмінний, як рядок
const buf = Buffer.from('test');
buf[0] = 0x48; // змінює 't' на 'H'
console.log(buf.toString()); // "Hest"Buffer - це мутабельний масив байтів. Якщо потрібна незмінна копія, створи її через Buffer.from(buf).
Де зустрічається в реальному коді
- Express + Multer: завантажені файли приходять як
req.file.buffer, хешуєш або скануєш перед записом на диск - Бібліотека Sharp:
sharp(buffer).resize().toBuffer()обробляє JPEG і PNG повністю в пам'яті - Модуль crypto:
crypto.createCipheriv(algorithm, keyBuffer, ivBuffer)для AES-шифрування fs.createReadStreamвидає Buffer-чанки; ти контролюєш коли декодувати у рядокnet.Socket.write(buffer)для сирих TCP-фреймів, шлях без копіювання
Питання для поглиблення
Q: Яка різниця між Buffer.alloc і Buffer.allocUnsafe?
A: alloc заповнює пам'ять нулями перед поверненням, безпечно але трохи повільніше. allocUnsafe пропускає обнулення для швидкості, але байти містять те, що було в тій пам'яті раніше. Використовуй allocUnsafe тільки якщо одразу перезаписуєш кожну позицію.
Q: Чому Buffer виділяється поза купою V8?
A: Щоб уникнути пауз збирача сміття. Якщо тримати 1 ГБ відеопотоку всередині купи V8, GC мусить сканувати і відстежувати його. Пам'ять під управлінням libuv невидима для GC, тому великі бінарні дані не викликають пауз.
Q: Buffer проти Uint8Array в Node.js 20. Що обрати?
A: Buffer - підклас Uint8Array, вони взаємосумісні. Для I/O API Node.js (потоки, crypto, http) Buffer залишається природним вибором, бо API повертає Buffer. Для нового утилітарного коду без I/O підходить Uint8Array, він більш портативний для браузерного середовища. При стабільному I/O на файлах від 1 ГБ Buffer backed by libuv уникає GC-пауз, які може викликати Uint8Array.
Q: Як працює Buffer.concat і чи є дешевший варіант?
A: Buffer.concat([buf1, buf2]) виділяє новий Buffer і копіює всі дані туди, O(n) від загального розміру. Zero-copy concat не існує. Якщо потрібно лише читати через кілька буферів, тримай їх у масиві і відстежуй зміщення вручну.
Q: Що станеться, якщо передати великий Buffer у writable stream без обробки backpressure?
A: Внутрішній буфер потоку заповниться. Якщо споживач повільний, дані накопичуватимуться в пам'яті доки процес не вичерпає її. Рішення: використовувати stream.pipeline() або перевіряти повертане значення write() і ставити джерело на паузу, коли воно повертає false.
Приклади
Базовий: рядок у Buffer і назад
const str = 'Hello';
const buf = Buffer.from(str, 'utf8');
console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(buf.length); // 5
console.log(buf[0]); // 72 (десятковий для 0x48)
console.log(buf.toString('utf8')); // "Hello"
console.log(buf.toString('hex')); // "48656c6c6f"
console.log(buf.toString('base64')); // "SGVsbG8="Buffer.from(str, 'utf8') кодує кожен символ у відповідне байтове представлення UTF-8. toString декодує назад. Для чистого ASCII як "Hello" кількість байтів збігається з кількістю символів. Додай не-ASCII символ і кількість байтів зросте.
Практичний: обробник завантаження файлу з перевіркою хешу
const crypto = require('crypto');
const fs = require('fs');
// Multer зберігає завантажений файл як req.file.buffer
app.post('/upload', (req, res) => {
const fileBuffer = req.file.buffer;
if (fileBuffer.length > 1_000_000) {
return res.status(413).send('Файл занадто великий');
}
const hash = crypto
.createHash('sha256')
.update(fileBuffer)
.digest('hex');
fs.writeFileSync(`uploads/${hash}.jpg`, fileBuffer);
res.send({ saved: hash });
});Байти файлу залишаються як Buffer від отримання по HTTP до запису на диск. Жодного проміжного перетворення в рядок, жодних проблем з кодуванням. Хеш обчислюється безпосередньо на бінарних даних - саме це потрібно для перевірки цілісності.
Поглиблений: крайній випадок зрізу при багатобайтовому Unicode
const emoji = '😊👍';
const buf = Buffer.from(emoji, 'utf8');
console.log(buf.length); // 8 байтів (кожна emoji займає 4 байти в UTF-8)
console.log(emoji.length); // 2 (JS рахує кодові одиниці, не байти)
// Неправильно: байтове зміщення 3 всередині першої emoji
const broken = buf.slice(0, 3);
console.log(broken.toString('utf8')); // пошкоджений вивід
// Правильно: повна межа 4 байти
console.log(buf.toString('utf8', 0, 4)); // "😊"
console.log(buf.toString('utf8', 4, 8)); // "👍"Це підловлює розробників, які припускають що кількість байтів дорівнює кількості символів. Для багатобайтових символів завжди обчислюй зміщення через Buffer.byteLength(str, 'utf8') або спочатку працюй на рівні рядка, потім конвертуй у Buffer.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.