nodejs
Серверная разработка на Node.js — событийный цикл (что блокирует, микро/макротаски), async-паттерны (Promise.all vs allSettled, AbortController-таймауты), поиск утечек памяти (--inspect, heap snapshot
SKILL.mdskills/nodejs/SKILL.md
GitHubНавык: Node.js
Событийный цикл
- Один поток исполняет JS; I/O (сеть, fs, DNS) уходит в libuv и не блокирует. Блокирует синхронный CPU-код:
JSON.parse/stringifyмегабайтных тел,fs.readFileSync, sync-крипто и sync-сжатие, regex с катастрофическим бэктрекингом, огромные циклы. - Порядок исполнения: синхронный код → микротаски (
process.nextTick— раньше всех, затем промисы/queueMicrotask) → макротаски по фазам (setTimeout/setInterval→ I/O-коллбэки →setImmediate). РекурсивныеnextTick/микротаски морят I/O голодом; «уступить цикл» — этоsetImmediate, неnextTick. - Диагностика зависаний:
monitorEventLoopDelay()изperf_hooksв метрики; задержка цикла стабильно >100ms — где-то синхронный счёт. Флаг--trace-sync-ioловит sync-I/O после старта. - CPU-тяжёлое (парсинг, хэши, изображения) — в
worker_threadsили async-версии API (crypto.pbkdf2,zlib.gzip), не sync.
Async-паттерны
Promise.allvsallSettled:all— «всё или ничего», первый reject роняет результат (незавершённые промисы при этом продолжают работать!);allSettled— когда нужны все исходы, включая ошибки:const [user, orders] = await Promise.all([getUser(id), getOrders(id)]) // зависимые данные: любая ошибка = ошибка всего const results = await Promise.allSettled(urls.map(fetchOne)) // независимые задачи: разбираем каждый исход const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value)- Таймауты и отмена — AbortController, а не «гонка» с setTimeout:
const res = await fetch(url, { signal: AbortSignal.timeout(5000) }) // встроенный таймаут const ac = new AbortController() // отмена по внешнему событию req.on('close', () => ac.abort()) await doWork({ signal: ac.signal }) // signal передавай вглубь всех вызовов - Ограничивай параллелизм (пул на N задач,
p-limit) — не пускайPromise.allна тысячи запросов разом. unhandledRejection/uncaughtException— обрабатывай (лог + graceful shutdown), не глотай.
Утечки памяти
Симптом: RSS/heap растёт от запроса к запросу и не отпускается после GC.
- Запусти с инспектором:
node --inspect app.js→ Chromechrome://inspect(илиnpx clinic heap). - Сними heap snapshot до нагрузки → прогони нагрузку → второй снапшот → режим Comparison: что выросло по Retained Size и кто держит ссылку (вкладка Retainers).
- Типовые виновники: глобальные Map/массивы-кэши без лимита (нужен LRU/TTL), слушатели без
removeListener(симптом — MaxListeners warning), замыкания, удерживающие большие объекты, незакрытые таймеры/сокеты. - Динамику без снапшотов —
process.memoryUsage()в метрики;--max-old-space-size— страховка, не лечение.
worker_threads vs cluster
| worker_threads | cluster / несколько процессов | |
|---|---|---|
| Задача | CPU-тяжёлые вычисления внутри сервиса | масштабирование HTTP-сервера на ядра |
| Память | общая возможна (SharedArrayBuffer), дешёвый обмен | изолирована; обмен только IPC/сеть |
| Падение | роняет только воркер | изолировано, supervisor перезапустит процесс |
| Когда | парсинг, сжатие, крипто, генерация PDF | production HTTP: PM2 / node:cluster / реплики k8s |
Воркеров держи пулом, не создавай на каждый запрос. В контейнерах для HTTP предпочитай реплики оркестратора, а не cluster внутри контейнера.
Стримы и backpressure
- Большие файлы/ответы — стримами, не буферизуй целиком в память.
pipeline(src, transform, dst)изstream/promises— сам пробрасывает ошибки и закрывает стримы (в отличие от голого.pipe()) и уважает backpressure: медленный потребитель притормаживает производителя.