Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Синхронний та асинхронний код у Node.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Синхронний vs асинхронний код у Node.js:** sync блокує event loop до завершення операції; async передає задачу libuv і повертається миттєво. ```js // Sync: loop заморожений під час читання файлу const data = fs.readFileSync('./file.txt', 'utf8'); // Async: loop залишається вільним const data = await fs.promises.readFile('./file.txt', 'utf8'); ``` **Головне правило:** ніколи не використовуй sync I/O в обробнику сервера. Одне блокуюче читання зупиняє всі паралельні запити.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Синхронний код** блокує event loop Node.js до завершення операції; **асинхронний** реєструє задачу в libuv і повертається миттєво, поки цикл обробляє інші запити. ## Теорія ### TL;DR - Уяви одного касира в магазині: sync змушує всіх чекати, поки один покупець розраховується з повним кошиком; async дозволяє касиру взяти замовлення на папірець і одразу обслужити наступного - Головна різниця: sync зупиняє весь event loop; async тримає його вільним для нових запитів - Node.js працює в одному потоці, тому заблокований loop означає нуль оброблених запитів у цей час - Правило: async для будь-чого пов'язаного з файлами, мережею або базою даних; sync прийнятний лише для коротких операцій у пам'яті або скриптів ### Швидкий приклад ```js const fs = require('fs'); // Sync: loop заморожений приблизно на 100ms console.log('Start'); const data = fs.readFileSync('./file.txt', 'utf8'); // Все зупинено тут console.log(data); console.log('End'); // Виведе: Start -> вміст файлу -> End // Async: loop залишається вільним console.log('Start'); fs.readFile('./file.txt', 'utf8', (err, data) => { if (err) return console.error(err); console.log(data); // Виконається пізніше, коли ОС завершить читання }); console.log('End'); // Виведе: Start -> End -> вміст файлу ``` Синхронна версія заморожує все, поки Node чекає на диск. Асинхронна передає роботу libuv, повертається миттєво, і колбек запускається щойно файл готовий. ### Ключова різниця Node.js має один головний потік. Коли виконується синхронний I/O, цей потік просто чекає на диск або мережу, нічого більше не обробляючи. Асинхронний I/O делегує очікування пулу потоків libuv (за замовчуванням 4 потоки), тому головний потік продовжує отримувати нові задачі з event loop. Колбек потрапляє до черги задач щойно ОС сигналізує про завершення, і loop запускає його після того як call stack порожній. ### Коли що використовувати - **CLI-скрипт з одним файлом:** sync нормально, більше нічого не чекає - **API-сервер під навантаженням:** тільки async; синхронне читання на 500ms блокує всі паралельні запити - **Парсинг JSON вже в пам'яті:** sync нормально, це чисті CPU-обчислення без I/O - **Запит до бази даних або HTTP-дзвінок:** async, мережевий round trip займає 50-200ms - **Одноразовий тестовий або дебаг-скрипт:** sync спрощує читання коду ### Порівняння async-патернів | Патерн | Обробка помилок | Читабельність | Коли використовувати | |--------|----------------|---------------|----------------------| | Callbacks | Перший аргумент `err` | Вкладені, важко читати | Legacy-код, старіші Node API | | Promises | `.catch()` | Ланцюгові виклики | Послідовні async-потоки | | async/await | `try/catch` | Лінійний, найпростіший | Більшість продакшн-коду сьогодні | Всі три патерни роблять одне й те саме під капотом. Різниця лише в синтаксисі колбеку, що викликається після завершення роботи libuv. ### Як Node обробляє async всередині V8 виконує JavaScript у call stack головного потоку. Коли ти викликаєш `fs.readFile`, Node передає запит libuv, а той передає його ОС або власному пулу потоків. Коли файл готовий, libuv додає колбек до фази poll в event loop. Після того як call stack порожніє, event loop підхоплює колбек і запускає твій код. Promises та async/await використовують microtask queue, яка спустошується перед наступним тіком event loop. Саме тому `await` відчувається як синхронний код, хоча насправді виконання призупиняється і відновлюється непомітно. ### Типові помилки **1. Синхронний I/O всередині HTTP-обробника** ```js // Неправильно: блокує кожен запит на час читання app.get('/data', (req, res) => { const content = fs.readFileSync('./bigfile.json'); // Сервер завис res.send(content); }); // Правильно: loop залишається вільним app.get('/data', async (req, res) => { const content = await fs.promises.readFile('./bigfile.json'); res.send(content); }); ``` Цей патерн найчастіше зустрічається з конфіг-файлами, які хтось вирішив читати прямо в обробнику. У скрипті це безпечно, на сервері під трафіком приносить сервер. Виправлення займає один рядок. **2. Послідовні await у циклі** ```js // Неправильно: файли читаються по одному, час = сума всіх читань for (const file of files) { const data = await fs.promises.readFile(file); // Серіалізовано } // Правильно: всі читання стартують одночасно const results = await Promise.all( files.map(f => fs.promises.readFile(f, 'utf8')) ); ``` `await` всередині `for` серіалізує операції. 10 файлів по 100ms кожен означає 1000ms замість приблизно 100ms. **3. Забутий аргумент помилки в колбеку** ```js // Неправильно: при ENOENT крашиться, бо `data` насправді є об'єктом помилки fs.readFile('./file.txt', (data) => console.log(data)); // Правильно fs.readFile('./file.txt', 'utf8', (err, data) => { if (err) return console.error(err); console.log(data); }); ``` Конвенція колбеків у Node завжди `(err, data)`. Якщо пропустити `err`, ти обробляєш об'єкт помилки як вміст файлу, і процес падає на наступній операції. **4. Callback hell** ```js // Неправильно: піраміда смерті, помилки важко відловити fs.readFile(file1, (err1, data1) => { fs.readFile(file2, (err2, data2) => { fs.readFile(file3, (err3, data3) => { // ... }); }); }); // Правильно const [data1, data2, data3] = await Promise.all([ fs.promises.readFile(file1, 'utf8'), fs.promises.readFile(file2, 'utf8'), fs.promises.readFile(file3, 'utf8'), ]); ``` ### Де зустрічається в реальному коді - **Express.js:** обробники маршрутів використовують `async/await` для запитів до бази (`await User.findById(id)`) і читання файлів - **NestJS:** guards та interceptors асинхронні за замовчуванням для перевірки авторизації - **Fastify:** плагіни підключаються до async-lifecycle з `fastify.get('/', async (req, reply) => {...})` - **Webpack (Node-інструменти збірки):** лоадери компілюють JS і CSS асинхронно, не блокуючи бандлер - **Будь-який продакшн-сервер:** `fs.promises`, `axios` та більшість ORM повертають Promises за замовчуванням ### Питання до теми **Q:** Чому однопотоковий Node.js справляється з тисячами одночасних запитів? **A:** Event loop залишається неблокованим, бо I/O очікування обробляє пул потоків libuv та ОС. Сам JavaScript ніколи не блокується, тому loop постійно обробляє нові події. **Q:** Яка різниця між callbacks, Promises та async/await? **A:** Callbacks - найстаріший патерн, утворюють глибоку вкладеність при складних потоках. Promises дозволяють `.then()`-ланцюжки і кращу передачу помилок. Async/await - синтаксичний цукор над Promises, що читається як синхронний код. Всі три компілюються в один механізм. **Q:** Що відбувається при CPU-інтенсивних задачах? **A:** Event loop все одно блокується, бо CPU-робота виконується в головному потоці незалежно від async-синтаксису. Рішення - модуль `worker_threads` (доступний з Node 10) або `cluster` для кількох процесів. **Q:** Чим відрізняються `process.nextTick` і `setImmediate`? **A:** `process.nextTick` спрацьовує до наступної фази event loop, раніше за I/O колбеки. `setImmediate` спрацьовує у фазі check, після I/O. Рекурсивне використання `process.nextTick` може заблокувати I/O колбеки і затримати читання файлів. **Q:** Як налаштувати розмір пулу потоків libuv? **A:** Встановлюй змінну оточення `UV_THREADPOOL_SIZE` перед стартом Node. За замовчуванням 4 потоки, максимум 1024. Важкі crypto або database навантаження часто потребують більшого пулу, наприклад `UV_THREADPOOL_SIZE=16 node server.js`. ## Приклади ### Порядок виведення: sync проти async ```js const fs = require('fs'); const fsPromises = require('fs').promises; // --- Sync --- console.log('A'); const raw = fs.readFileSync('./file.txt', 'utf8'); console.log('B', raw.slice(0, 10)); console.log('C'); // Виведе: A -> B (вміст) -> C // Loop був заблокований між A і B. // --- Async з async/await --- async function run() { console.log('A'); const content = await fsPromises.readFile('./file.txt', 'utf8'); console.log('B', content.slice(0, 10)); console.log('C'); } run(); // Всередині функції: A -> B (вміст) -> C // Але між A і await інший код міг виконуватись у event loop. ``` Порядок всередині async-функції виглядає однаково, але event loop був вільний для інших задач під час `await`. В цьому і є вся суть. ### Express-маршрут: async читання без блокування ```js const express = require('express'); const fs = require('fs').promises; const app = express(); // Поки файл читається, сервер приймає інші запити app.get('/profile/:user', async (req, res) => { try { const raw = await fs.readFile( `./profiles/${req.params.user}.json`, 'utf8' ); res.json(JSON.parse(raw)); } catch (err) { res.status(404).json({ error: 'Профіль не знайдено' }); } }); app.listen(3000); ``` Два запити надходять одночасно. Обидва викликають `await fs.readFile`. Обидва повертають управління event loop, поки ОС читає файли. Жоден не блокує інший, і обидва відповідають щойно їхній файл готовий. ### Паралельне читання конфігів з Promise.all ```js const fs = require('fs').promises; async function loadConfig() { // Послідовно: ~300ms для 3 файлів по ~100ms кожен // const db = await fs.readFile('./db.json', 'utf8'); // const cache = await fs.readFile('./cache.json', 'utf8'); // const app = await fs.readFile('./app.json', 'utf8'); // Паралельно: ~100ms загалом, всі три читання стартують одразу const [db, cache, app] = await Promise.all([ fs.readFile('./db.json', 'utf8'), fs.readFile('./cache.json', 'utf8'), fs.readFile('./app.json', 'utf8'), ]); return { db: JSON.parse(db), cache: JSON.parse(cache), app: JSON.parse(app), }; } ``` `Promise.all` запускає всі три читання одночасно і чекає на найповільніше. Загальний час приблизно рівний часу одного найдовшого читання, а не їхній сумі.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.