Що таке Node.js і як він працює?
Node.js - це середовище виконання JavaScript, яке запускає JS-код поза браузером, на сервері.
Теорія
TL;DR
- Node.js - не фреймворк і не мова, а середовище виконання (runtime) для JavaScript
- Складається з V8 (JS-движок Chrome) + libuv (C++-бібліотека для асинхронного I/O)
- Один JS-потік + неблокуючий I/O: код не чекає на завершення файлових чи мережевих операцій
- Підходить для I/O-навантажених задач (API, реальний час, стрімінг); погано справляється з важкими обчисленнями
- Екосистема npm: понад 2 мільйони пакетів
Швидкий приклад
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Node.js');
});
server.listen(3000, () => {
console.log('Listening on port 3000');
});HTTP-сервер на 10 рядків. Без Apache, без Nginx. Node.js сам управляє мережею через вбудований модуль http.
Як Node.js обробляє запити
JS-потік один. Одна точка виконання в будь-який момент. Але що відбувається, коли 1000 користувачів одночасно звертаються до сервера?
Node.js делегує блокуючу роботу - читання файлів, запити до БД, мережеві виклики - бібліотеці libuv. libuv утримує пул потоків (4 за замовчуванням), який виконує реальний I/O на рівні ОС. Коли операція завершена, libuv додає callback у чергу подій. Event loop (цикл подій) забирає його і виконує на JS-потоці.
Ваш JS-код
|
Node.js APIs (fs, http, net, crypto...)
|
libuv (event loop + thread pool)
|
ОС (файлова система, мережа, таймери)JS-потік вільний увесь час, поки ОС виконує реальну роботу. Саме тому Node.js обробляє тисячі одночасних з'єднань на одному потоці - здебільшого він просто чекає на I/O, а не обчислює.
V8 і libuv
V8 компілює JavaScript у нативний машинний код. Не інтерпретує, не проганяє через повільний байткод - компілює одразу. Саме тому Node.js швидкий у виконанні JS.
libuv - друга половина рівняння. Це C++-бібліотека, написана спеціально для Node.js, але зараз використовується й іншими проектами. Вона забезпечує:
- цикл подій (event loop)
- асинхронний файловий I/O (пул потоків, 4 за замовчуванням, налаштовується через
UV_THREADPOOL_SIZE) - TCP/UDP сокети
- DNS-резолвінг
- таймери (
setTimeout,setInterval)
Без libuv Node.js був би просто V8 без можливості асинхронно взаємодіяти з ОС.
Коли Node.js не справляється
Один JS-потік - реальне обмеження. Запусти важку CPU-операцію - парсинг великого JSON, обробку зображень, ML-інференс - і event loop заблокується. Всі інші запити чекатимуть.
// Блокує event loop для всіх користувачів, поки виконується
app.get('/report', (req, res) => {
const result = generateHeavyReport(); // блокує JS-потік
res.json({ result });
});Для CPU-інтенсивних задач використовуй Worker Threads (доступні з Node 12) або винось роботу в окремий сервіс. Node.js добре працює як API-шлюз і координатор, але не як обчислювальний рушій.
Де Node.js доречний
- REST API і GraphQL-сервери
- WebSocket-сервери і застосунки реального часу (чат, лайв-оновлення, колаборативні інструменти)
- CLI-інструменти і build-скрипти (webpack, Vite, ESLint - все на Node)
- Serverless-функції (AWS Lambda, Vercel, Cloudflare Workers)
- Мікросервіси, що переважно виконують I/O
Екосистема npm - частина цінності. Понад 2 мільйони пакетів означають, що більшість задач вже мають готове рішення.
Типові помилки
Блокування event loop синхронними операціями
// Погано - блокує весь сервер під час читання
const data = fs.readFileSync('./large-file.json');
// Добре - libuv бере на себе, JS-потік залишається вільним
fs.readFile('./large-file.json', (err, data) => {
// виконається, коли файл буде готовий
});Припущення, що Node.js багатопотоковий
Node.js має один JS-потік. setTimeout і setInterval не виконуються паралельно - вони ставлять callback у чергу. Якщо event loop заблокований, таймери спрацьовують із запізненням.
Використання Node.js для CPU-важких задач без Worker Threads
Один CPU-bound запит може затримати відповідь для всіх інших користувачів. Потрібна важка CPU-робота - використовуй worker_threads або дочірній процес.
Ігнорування необроблених відмов Promise
Починаючи з Node 15, необроблений rejection завершує процес за замовчуванням. Завжди додавай .catch() або try/catch в async-функціях.
// Завершує процес у Node 15+
fetchUserData(userId); // async-функція, обробки помилок немаєДе зустрічається в реальних проектах
- Express, Fastify, Koa - HTTP-фреймворки на основі вбудованого
http-модуля Node.js - Next.js - SSR і API routes виконуються в Node.js-процесі
- Socket.io - комунікація в реальному часі через WebSocket
- NestJS - структурований бекенд-фреймворк для великих застосунків
- Інструментарій: Vite, webpack, компілятор TypeScript, ESLint, Prettier
Я бачив команди, що обирали Node.js для конвеєра обробки даних, де більшість часу йшла на очікування відповідей від БД. Node.js добре справився з конкурентністю без додаткової інфраструктури.
Follow-up питання
Q: Чим Node.js відрізняється від JS-движка браузера?
A: Обидва можуть використовувати V8, але Node.js додає libuv для доступу до ОС (файлова система, мережа) і не має DOM, window чи браузерних API. Браузер має document, fetch, localStorage. Node - fs, net, child_process.
Q: Чому Node.js однопотоковий, але обробляє багато з'єднань?
A: JS-виконання однопотокове, але I/O виконує пул потоків libuv і асинхронні виклики ОС. Більшість часу Node.js чекає на I/O, а не обчислює. Поки він чекає, event loop обробляє інші callback-и.
Q: Що відбувається, коли event loop заблокований?
A: Усі запити в черзі стоять. Якщо JS-потік виконує 5-секундне обчислення, всі відповіді затримуються на ці 5 секунд. Саме тому Node.js не підходить для CPU-важких задач без Worker Threads.
Q: Скільки потоків насправді використовує Node.js?
A: Один JS-потік. libuv додає 4 потоки в пулі за замовчуванням (змінюється через UV_THREADPOOL_SIZE). Мережевий I/O асинхронний на рівні ОС і не потребує пулу. Типовий Node-процес показує 6-8 потоків у top або htop.
Q: Чи є libuv специфічним для Node.js?
A: Ні. libuv - незалежна open-source C++-бібліотека (libuv/libuv на GitHub). Node.js був першопричиною її створення, але інші проекти також її використовують.
Приклади
Простий HTTP-сервер
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(3000, () => console.log('Сервер на порту 3000'));Без залежностей. Модуль http бере на себе TCP-з'єднання, парсинг HTTP-запитів і підтримує сервер активним.
Асинхронне читання файлу з обробкою помилок
const fs = require('fs').promises;
async function loadConfig(path) {
try {
const raw = await fs.readFile(path, 'utf8');
return JSON.parse(raw);
} catch (err) {
console.error('Не вдалось завантажити конфіг:', err.message);
return null;
}
}
loadConfig('./config.json').then(config => {
console.log('Назва застосунку:', config?.name);
});fs.promises.readFile передає роботу в пул потоків libuv. JS-потік залишається вільним до готовності файлу. await зупиняє тільки цю async-функцію, але не весь event loop.
Порядок виконання в event loop
console.log('1 - синхронно');
setTimeout(() => console.log('3 - макротаск'), 0);
Promise.resolve().then(() => console.log('2 - мікротаск'));
console.log('4 - синхронно');
// Виведе:
// 1 - синхронно
// 4 - синхронно
// 2 - мікротаск (Promise callback)
// 3 - макротаск (setTimeout)Мікротаски (Promise callback-и) виконуються раніше за макротаски (setTimeout), навіть якщо затримка 0 мс. Це дивує розробників, які очікують, що setTimeout(..., 0) спрацює одразу після поточного синхронного блоку.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.