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

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 краще для складної бізнес-логіки з багатопотоковістю

Коментарі

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

Колекції в Elixir

Списки Що таке список у Elixir У Elixir список — це впорядкована колекція елементів, реалізована як однозв’язний список . Це означає, що кожен елемент (вузол) зберігає посилання на наступний, але не на попередній. Така структура дозволяє швидко додавати елементи на початок списку, але повільно доступати до довільного елементу. У Java подібну структуру представляє LinkedList — частина Java Collections Framework. Вона реалізована як двозв’язний список, що забезпечує зручне додавання/видалення елементів з початку або кінця списку. Створення списків У Elixir список створюється за допомогою квадратних дужок: list = [1, 2, 3, 4] Додавання елементів У Elixir новий елемент можна додати тільки на початок списку за допомогою оператора | : # Elixir list = [2, 3, 4] new_list = [1 | list] # [1, 2, 3, 4] Доступ до елементів У Elixir немає прямого доступу до елементів за індексом, але це можна зробити через Enum.at : Enum.at([10, 20, 30], 1) # 20 Ітерація по списку У Elixir ...

Інструменти для роботи з 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...

Атоми в мові програмування Elixir

Атоми в Elixir Атоми є фундаментальною концепцією в Elixir , що відіграє ключову роль у створенні надійних та масштабованих систем. В Elixir це специфічний тип даних, який є константою , незмінною , ідентифікованою за своїм ім'ям . Отже, атом в Elixir — це іменована константа, що представляє себе. Уявіть, що ви даєте унікальне ім'я певній речі, і це ім'я завжди посилається саме на цю річ, і ніколи на щось інше. Наприклад, атом :ok завжди буде означати саме успішне завершення операції, а не якесь інше значення. Технічно, атоми є похідними від чисел . Кожен унікальний атом зберігається у таблиці атомів, і йому присвоюється унікальний цілочисельний ідентифікатор. Це робить їх надзвичайно ефективними для порівняння: замість порівняння рядків (що є повільною операцією), Elixir порівнює цілочисельні ідентифікатори. Переваги та особливості використання атомів Переваги атомів: Ефективність. Завдяки своєму числовому представленню, порівняння атомів є дуже швидким. Це осо...

Встановлення PostgreSQL на Ubuntu-сервер

Встановлення Оновлюємо пакети та встановлюємо PostgreSQL: sudo apt update sudo apt install -y postgresql postgresql-contrib Перевіряємо статус сервісу: sudo systemctl status postgresql Якщо PostgreSQL не запущений, запустимо його: sudo systemctl start postgresql sudo systemctl enable postgresql Налаштування безпеки Зміна пароля: sudo -u postgres psql У консолі PostgreSQL: ALTER USER postgres PASSWORD 'міцний_пароль'; \q \q - вихід з консолі. Список основних команд для роботи з PostgreSQL можна переглянути за посиланням. За замовчуванням PostgreSQL слухає localhost (127.0.0.1). Щоб дозволити доступ із зовнішніх машин, редагуємо конфігурацію: sudo nano /etc/postgresql/17/main/postgresql.conf (замість 17 вкажи версію PostgreSQL, яку встановлено) Шукаємо рядок: #listen_addresses = 'localhost' та замінюємо на listen_addresses = '*' Зберігаємо (Ctrl + X, Y, Enter). Тепер редагуємо pg_hba.conf: sudo nano /etc/postgresql/17/main/pg_hba.conf...

Основи Node.js

Що таке Node.js? Node.js — це середовище виконання JavaScript поза браузером, побудоване на рушії Google V8 . Воно дозволяє запускати JavaScript на сервері, створюючи серверні застосунки з високою продуктивністю. Основні характеристики: Однопотокова модель з неблокуючим I/O Асинхронне виконання за допомогою event loop Висока продуктивність у роботі з мережевими запитами npm — найбільший реєстр пакетів Що таке event loop? Event loop — це механізм в Node.js, який дозволяє неблокуючим асинхронним операціям виконуватись у середовищі з єдиним потоком. Він постійно перевіряє наявність подій у черзі та викликає відповідні колбеки. Як працює однопоточність у Node.js? Node.js використовує один потік (main thread) для обробки JavaScript-коду. Операції, які займають час (мережеві запити, читання з файлової системи), делегуються до системних API або thread pool, і після завершення результат повертається у основний потік через event loop. Приклад: асинхронна о...