Skip to main content

Що таке 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 мільйони пакетів

Швидкий приклад

js
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 заблокується. Всі інші запити чекатимуть.

js
// Блокує 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 синхронними операціями

js
// Погано - блокує весь сервер під час читання 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-функціях.

js
// Завершує процес у 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-сервер

js
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-запитів і підтримує сервер активним.

Асинхронне читання файлу з обробкою помилок

js
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

js
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) спрацює одразу після поточного синхронного блоку.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?