Перейти до основного вмісту

Event Loop в Node.js

Event Loop в Node.js: архітектура, фази, черги

Event Loop — це серце асинхронності в Node.js. Якщо ви знайомі з Java, то знаєте про багатопотоковість та Thread Pool. Node.js працює інакше: замість створення окремих потоків для кожного запиту, він використовує єдиний потік і Event Loop для обробки всіх операцій.

Порівняння з Java/Spring Boot

Java/Spring Boot (традиційна модель):

  • Кожен HTTP-запит обробляється окремим потоком з Thread Pool
  • Якщо потік блокується на I/O операції (база даних, файл), він чекає
  • Обмежена кількість потоків (наприклад, 200) = максимум 200 одночасних запитів

Node.js (асинхронна модель):

  • Один основний потік (Event Loop)
  • I/O операції делегуються системі (libuv), потік не блокується
  • Може обробляти тисячі одночасних підключень на одному потоці
// Java - блокуючий виклик
String data = readFile("file.txt"); // потік чекає
System.out.println(data);

// Node.js - неблокуючий виклик
fs.readFile('file.txt', (err, data) => {
  console.log(data); // виконається пізніше
});
console.log('Продовжую роботу'); // виконається одразу

Архітектура Event Loop

Event Loop в Node.js базується на бібліотеці libuv, яка надає асинхронні I/O операції для різних операційних систем.

Основні компоненти:

  • Call Stack — стек викликів JavaScript (як у Java)
  • Node APIs — нативні асинхронні API (fs, http, timers)
  • Event Loop — механізм, що координує виконання коду
  • Callback Queue — черга для callback'ів
  • Microtask Queue — пріоритетна черга для Promise

Фази Event Loop

Event Loop працює циклічно, проходячи через шість фаз у порядку:

   ┌───────────────────────────┐
┌─>│           timers          │ <- setTimeout, setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │ <- I/O callbacks (TCP errors)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │ <- внутрішні операції
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │ <- нові I/O події, виконання I/O callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │ <- setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │ <- socket.on('close', ...)
   └───────────────────────────┘

1. Фаза Timers

Виконує callback'и, заплановані через setTimeout() та setInterval().

console.log('1. Старт');

setTimeout(() => {
  console.log('3. Timeout виконано');
}, 0);

console.log('2. Синхронний код');

// Вивід:
// 1. Старт
// 2. Синхронний код
// 3. Timeout виконано

Аналогія з Java: схоже на ScheduledExecutorService.schedule(), але без окремого потоку.

2. Фаза Poll

Найважливіша фаза. Тут Event Loop:

  • Отримує нові I/O події
  • Виконує I/O-related callbacks (крім close, timers, setImmediate)
  • Може блокуватися в очікуванні нових подій
const fs = require('fs');

fs.readFile('data.txt', (err, data) => {
  console.log('Файл прочитано'); // Poll phase
});

3. Фаза Check

Виконує callback'и setImmediate(). Ця функція унікальна для Node.js.

setImmediate(() => {
  console.log('Immediate');
});

setTimeout(() => {
  console.log('Timeout');
}, 0);

// Порядок може відрізнятися!
// У I/O циклі setImmediate завжди виконується раніше

Microtasks vs Macrotasks

Це ключова концепція для розуміння пріоритетів виконання.

Macrotasks (Task Queue):

  • setTimeout / setInterval
  • setImmediate
  • I/O операції

Microtasks (Microtask Queue):

  • Promise.then / catch / finally
  • process.nextTick (найвищий пріоритет!)
  • queueMicrotask

Правило виконання: після кожної macrotask Event Loop виконує ВСІ microtasks перед наступною macrotask.

console.log('1. Синхронний');

setTimeout(() => {
  console.log('5. setTimeout (macrotask)');
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise (microtask)');
});

process.nextTick(() => {
  console.log('2. nextTick (найвищий пріоритет)');
});

setImmediate(() => {
  console.log('6. setImmediate (macrotask)');
});

Promise.resolve().then(() => {
  console.log('4. Promise 2 (microtask)');
});

// Вивід:
// 1. Синхронний
// 2. nextTick (найвищий пріоритет)
// 3. Promise (microtask)
// 4. Promise 2 (microtask)
// 5. setTimeout (macrotask)
// 6. setImmediate (macrotask)

process.nextTick — особливий випадок

process.nextTick() має найвищий пріоритет і виконується ДО microtasks.

Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

// Вивід:
// nextTick
// Promise

Увага: надмірне використання process.nextTick() може заблокувати Event Loop!

// ПОГАНО - блокує Event Loop
function recursiveNextTick() {
  process.nextTick(recursiveNextTick);
}
recursiveNextTick(); // Event Loop ніколи не перейде до наступної фази

Практичний приклад з HTTP сервером

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  console.log('1. Запит отримано (Poll phase)');
  
  // Microtask
  Promise.resolve().then(() => {
    console.log('2. Promise - microtask');
  });
  
  // Macrotask
  setTimeout(() => {
    console.log('4. setTimeout - macrotask');
  }, 0);
  
  // process.nextTick
  process.nextTick(() => {
    console.log('3. nextTick - найвищий пріоритет');
  });
  
  // Асинхронна I/O операція
  fs.readFile('data.txt', (err, data) => {
    console.log('5. Файл прочитано (Poll phase, наступна ітерація)');
    res.end(data);
  });
});

server.listen(3000);

Порівняння з Spring Boot @Async

У Spring Boot для асинхронності використовується анотація @Async:

// Spring Boot - окремий потік з ThreadPoolTaskExecutor
@Async
public CompletableFuture<String> processAsync() {
    // Виконується в окремому потоці
    return CompletableFuture.completedFuture("Done");
}

// Node.js - той самий потік, але неблокуюча операція
async function processAsync() {
    // Event Loop делегує I/O, продовжує інші задачі
    const result = await someAsyncOperation();
    return result;
}

Візуалізація черг

┌─────────────────────────────────────────┐
│          Call Stack (основний потік)    │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   process.nextTick Queue (пріоритет 1)  │ ← виконується завжди першою
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Microtask Queue (пріоритет 2)         │ ← Promise.then/catch
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Macrotask Queue (пріоритет 3)         │ ← setTimeout, setImmediate, I/O
└─────────────────────────────────────────┘

Поширені помилки

1. Блокування Event Loop синхронним кодом:

// ПОГАНО - блокує Event Loop
const data = fs.readFileSync('large-file.txt'); // синхронна операція

// ДОБРЕ - не блокує Event Loop
fs.readFile('large-file.txt', (err, data) => {
  // асинхронна операція
});

2. Нерозуміння порядку виконання:

// Код
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

// Багато хто думає: timeout, promise
// Насправді: promise, timeout (microtask має пріоритет!)

Коли використовувати що

Механізм Використання
setTimeout / setInterval Затримка виконання, періодичні задачі
setImmediate Виконання після завершення поточної фази Poll
process.nextTick Критично важливі операції, що мають виконатися негайно (використовуйте обережно!)
Promise Асинхронні операції, що повертають результат

Практичні рекомендації

  • Уникайте синхронних операцій у продакшені (readFileSync, execSync)
  • Використовуйте Promise/async-await замість callback'ів
  • Обмежуйте process.nextTick — він може заблокувати Event Loop
  • Для CPU-intensive задач використовуйте Worker Threads або винесіть в окремий сервіс
  • Моніторте Event Loop lag за допомогою бібліотек як blocked або event-loop-lag

Висновки

Event Loop — це те, що робить Node.js ефективним для I/O-intensive додатків. Розуміння його роботи критично важливе для написання продуктивного коду.

Ключові відмінності від Java:

  • Node.js: один потік + асинхронність = висока пропускна здатність для I/O
  • Java: багато потоків + блокування = простіша модель, але менша ефективність для I/O
  • Node.js погано підходить для CPU-intensive задач (криптографія, обробка зображень)
  • Java/Spring Boot краще для складної бізнес-логіки з багатопотоковістю

Коментарі

Популярні публікації

Дамп бази даних Postgresql локально

Що потрібно встановити локально (Ubuntu) Встанови клієнтські утиліти PostgreSQL: pg_dump , psql , pg_restore . sudo apt update sudo apt install postgresql-client Кроки міграції Ідея: не зберігати дамп на серверах. Зробити дамп з віддаленого сервера на свій локальний ПК, потім передати його на новий сервер і там відновити. 1) Створення дампу з віддаленого сервера локально pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB -f dump_file.dump Пояснення ключів: -h (хост), -U (користувач), -Fc (custom format), -d (БД), -f (вихідний файл локально). 1.1) Варіант з розбиттям на частини (якщо треба) pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB | split -b 10G - dump_file.dump.part_ Це створить локально файли типу dump_file.dump.part_aa , dump_file.dump.part_ab тощо. 2) Передача дампу на новий сервер Один файл: scp dump_file.dump NEW_USER@NEW_SERVER_IP:/tmp/ Або частинами: scp dump_file.dump.part_* NEW_USER@NEW_SERVER_IP:/tmp/ 3) Збирання файл...

Основи Next.js

Що таке Next.js? Next.js — це прогресивний фреймворк для розробки веб-застосунків на основі React. Він розширює можливості React, додаючи готову інфраструктуру для Server-Side Rendering (SSR), генерації статичних сторінок, маршрутизації через файлову систему, а також можливість створювати backend-ендпоінти прямо в проєкті. Чому він став популярним? Next.js швидко здобув популярність завдяки тому, що пропонує: SEO-дружні сторінки завдяки SSR / SSG; швидкий перший рендер (First Page Load); просту маршрутизацію без налаштування; розгортання в один клік через Vercel; вбудовану оптимізацію зображень та продуктивності. Чим відрізняється від класичного React У класичному React ми отримуємо лише клієнтську бібліотеку для побудови UI-компонентів. Усе інше (SSR, маршрути, API) треба налаштовувати вручну або через сторонні бібліотеки (наприклад, React Router, Redux, Express, etc.). Next.js — це уже повноцінний фреймворк. Він одразу має: серверний рендеринг...

ООП у TypeScript

Короткий вступ TypeScript додає статичну типізацію поверх JavaScript і підтримує класичні концепти ООП: класи , успадкування , інтерфейси , інкапсуляцію , поліморфізм та абстракції . Нижче — практичні приклади та пояснення. Класи — декларація, конструктор, властивості та методи class Person { public name: string; private age: number; protected email?: string; // необов’язкове static species = 'Homo sapiens'; constructor(name: string, age: number, email?: string) { this.name = name; this.age = age; this.email = email; } public greet(): string { return `Hi, I'm ${this.name}.`; } private getBirthYear(currentYear: number): number { return currentYear - this.age; } } const p = new Person('Ivan', 30, 'ivan@example.com'); console.log(p.greet()); // console.log(p.age); // помилка: 'age' приватне console.log(Person.species); Успадкування і перевизначення (extends, super) class Employee extends Person { ...

Створення проєкту на TypeScript, модулі

Крок 1. Ініціалізація проєкту Спочатку створимо нову теку та ініціалізуємо Node.js-проєкт за допомогою npm : mkdir my-typescript-app cd my-typescript-app npm init -y Крок 2. Встановлення TypeScript Додаємо TypeScript як dev-залежність: npm install --save-dev typescript ts-node @types/node - typescript — компілятор TypeScript - ts-node — дозволяє запускати TS-файли без попередньої компіляції - @types/node — типи для стандартних Node.js API Крок 3. Створення файлу tsconfig.json Ініціалізуємо конфігурацію TypeScript: npx tsc --init Приклад файлу tsconfig.json { "compilerOptions": { "target": "ES2020", // Версія JS у вихідному коді "module": "CommonJS", // Система модулів (для Node.js) "rootDir": "src", // Вхідна папка з TypeScript-кодом "outDir": "dist", // Куди компілюється код JS "str...

Інструменти для роботи з Node.js

Що таке npm? npm (Node Package Manager) — це офіційний пакетний менеджер для Node.js . Він дозволяє: Встановлювати сторонні бібліотеки та фреймворки Керувати залежностями проєкту Запускати скрипти (команди) через package.json Приклад ініціалізації проєкту з npm npm init -y Файл package.json (скорочений приклад) { "name": "my-project", "version": "1.0.0", "scripts": { "start": "ts-node src/index.ts", "build": "tsc" }, "dependencies": { "express": "^4.18.0" }, "devDependencies": { "typescript": "^5.0.0", "ts-node": "^10.0.0" } } Що таке tsconfig.json? tsconfig.json — це файл конфігурації для компілятора TypeScript, який визначає, як слід компілювати код. Приклад файлу tsconfig.json { "compilerOptions": { "target": "ES2020...