Продуктивність API Node.js: фреймворк оптимізації з пріоритетним порядком
Дізнайтеся, як класифікувати способи покращення продуктивності API Node.js за рівнями зусиль та впливу, щоб спочатку усунути проблеми з пулінгом з’єднань та запитами типу N+1, перш ніж займатися екзотичними оптимізаціями.
Більшість статей про прискорення API Node.js пропонують простий список з п’ятнадцяти порад, де поруч із техніками пулінгу з’єднань до баз даних йде, наприклад, використання швидшого серіалізатора JSON, ніби кожен пункт заслуговує на однакову увагу. Це вводить в оману. Деякі рішення можна застосувати протягом десяти хвилин та значно скоротити час відповіді. Інші вимагають місяців роботи заради незначної користі. Розуміння цих відмінностей набагато важливіше, ніж запам’ятовування кожного пункту списку.
Рівень 1: Робіть це спочатку (висока ефективність, низькі зусилля)
Ці зміни коштують майже нічого у реалізації. Вони вимагають мінімум коду, несуть мало ризиків, і на практиці саме вони є причиною повільності значно частіше, ніж екзотичні рішення, до яких вдаються інші.
Увімкніть функцію keep-alive для всіх вихідних HTTP-запитів. За замовчуванням HTTP-клієнт Node відкриває нове з’єднання для кожного запиту, тому кожен запит до зовнішнього сервісу змушений знову оплачувати повну вартість процедури TCP-handshake та переговорів TLS. Повторне використання одного агента для кількох запитів усуває цей додатковий навантаження під час наступних викликів до того ж хоста.
const agent = new https.Agent({ keepAlive: true, maxSockets: 50 });
Завжди взаємодійте з вашою базою даних через пул з’єднань, а не через окреме з’єднання. Під справжньою конкурентною навантаженням одне з’єднання фактично перетворюється на чергу, за якою все очікує. Пул оптимального розміру дозволяє вашому API обробляти конкурентний трафік так, як це задумано — паралельно.
Дозвольте незалежні асинхронні виклики виконуватися паралельно, а не по черзі. Коли два оператори await не залежать один від результату іншого, немає причин змушувати їх чекати по черзі.
// costs the sum of both calls
const user = await getUser(id);
const orders = await getOrders(id);
// costs roughly the slower of the two
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);
Додайте індекси до стовпців, за допомогою яких ви фактично фільтруєте, об’єднуєте чи сортуєте дані. Серед усього, що перераховано, це, мабуть, найефективніший крок для будь-якого ендпоїнта, який читає дані з таблиці, що постійно росте, і його часто можна додати за один рядок коду.
Усуньте шаблони запитів N+1. Отримання списку та подальше виконання окремого запиту для кожного елемента в циклі здається безневинним при невеликій кількості тестових записів, але стає серйозною проблемою при роботі з тисячами реальних записів. Замініть цикл на один блоковий запит для отримання пов’язаних даних.
Додайте до кожного запиту, який інакше міг би повернути необмежений набір результатів, оператор LIMIT. Ендпоїнт, який повертає „усі замовлення, які цей клієнт коли-небудь здійснив“, без жодних обмежень, працює добре для абсолютно нового облікового запису, але припиняє функціонувати у тих, хто має багаторічну історію замовлень.
Рівень 2: Вартий справжніх інвестицій (високий ефект, значні зусилля)
Заходи на цьому рівні — це не дрібні корективи. Вони вимагають справжньої роботи з дизайном та часто нової інфраструктури, проте вирішують категорії проблем, до яких не можуть дійти рішення першого рівня.
Впровадіть шар кешування для дорогих та часто виконуваних операцій читання. Розміщення Redis перед повільним запитом на обробку даних або дорогим викликом API стороннього постачальника може скоротити час відповіді з 200 мс до приблизно 2 мс. Складність полягає не у створенні кешу, а у розробці ефективної стратегії анулювання даних у кеші, щоб він ніколи не надавав застарілі результати.
Повністю вийміть повільні, некритичні завдання з циклу запит-відповідь. Надсилання підтверджувального електронного листа, створення звіту, оновлення аналітичних даних — жодне з цих завдань не мусить бути завершене перед тим, як ви відповісте клієнту. Використання черги, такої як BullMQ чи SQS, разом із спеціалізованим процесом-роботом може скоротити час виконання запиту, який раніше займав 2 секунди, до приблизно 80 мілісекунд.
Перейдіть від пагінації на основі офсету до пагінації з використанням курсора для великих або глибоких наборів результатів. Пагінація, заснована на OFFSET, стає все повільнішою з кожною наступною сторінкою, оскільки база даних все одно мусить просканувати всі рядки, що були до неї. Підхід з використанням курсора залишається приблизно таким самим незалежно від того, чи знаходитеся ви на 5-й сторінці, чи на 5 000-й.
Розширюйте можливості горизонтально за допомогою справжнього балансувальника навантаження та централізованого стану сеансів чи кешу. Незалежно від того, наскільки добре ви налаштували систему, окремий процес Node рано чи пізно досягне свого ліміту. Робота кількох інстанцій за балансувальником навантаження, кожна з яких використовує спільний кеш Redis та пул з’єднань, дозволяє підняти цей ліміт, не вимагаючи від кожного окремого процесу бути швидшим.
Профіль перед подальшою оптимізацією. Як тільки очевидні проблеми вирішені, припущення щодо того, що сповільнює роботу, більше не є надійною стратегією. Використання справжнього інструменту профілювання або засобу APM, такого як clinic.js, хостований продукт APM, або навіть виконання команди EXPLAIN ANALYZE для підозрілого запиту, дозволяє побачити, де насправді витрачається час, а не там, де ви припускаєте.
Рівень 3: Зазвичай не варто надавати пріоритету (незначний вплив, часто переоцінюється)
Ці питання постійно згадуються під час обговорень продуктивності, проте вони рідко мають суттєвий вплив на справжній продакшн-API, головним чином тому, що вони стосуються частин стеку, які з самого початку не були справжніми вузькими місцями.
Деталізована налаштування серіалізації JSON. Існують більш швидкі бібліотеки JSON, які дійсно допомагають, але лише у масштабах, яких переважна більшість API ніколи не досягає. Якщо вашою справжньою проблемою є запит до бази даних, який триває 300 мс, скорочення кількох мілісекунд під час серіалізації — це рішення неправильної проблеми.
Заміна фреймворків виключно для зменшення їхнього навантаження. Різниця у продуктивності між Express та нібито швидшим альтернативним фреймворком справді існує, але вона незначна порівняно з витратами, які спричиняють ненадані індекси таблиць чи запити типу N+1. Вибір фреймворка може мати значення з багатьох інших причин; чиста швидкість зазвичай не є однією з них.
Спочатку намагатися використати техніки кластеризації, перш ніж займатися чимось іншим. Запуск кількох процесів Node для використання додаткових ядер CPU справді допомагає у завданнях, що сильно залежать від потужності CPU. Це не допоможе ендпоїнту, який працює повільно через очікування на нединамічну запит, адже очікування залишається очікуванням, незалежно від того, скільки процесів простоють у цей час.
Переписування коду, який активно використовується, мовою нижчого рівня виключно заради покращення продуктивності. Існують обґрунтовані випадки для цього, коли конкретна, перевірена проблема, пов’язана з обмеженнями CPU, є підставою. Однак значно частіше це роблять, перш ніж хтось насправді підтвердить, що саме там витрачається час, перетворюючи ефективну техніку на марні зусилля, спрямовані не на ту мету.
Як насправді використовувати це
Спочатку усуньте всі проблеми рівня 1, адже вони недорогі, мають низький рівень ризику та вирішують більшість проблем продуктивності в реальних умовах. Переходьте до рівня 2 лише для конкретних ендпоїнтів, де аналіз показує, що рівень 1 був недостатнім, а не як загальний підхід до переписування коду. Не торкайтеся проблем рівня 3, поки у вас не буде конкретних доказів від інструменту аналізу, а не припущень, що саме одна з цих технік є справжньою перешкодою. Більшість API, які працюють повільно, такими є через нерозв’язану проблему рівня 1, а не через відсутність якоїсь незрозумілої оптимізації, взятої з якогось блогу.
Пов’язана література
- Express проти Fastify у 2026 році: практичне порівняння фреймворків Node.js — У цьому посібнику порівнюються Express та Fastify за показниками продуктивності, перевірки даних, екосистеми та обробки помилок; також розглядаються основні зміни в Express 5, які можуть вплинути на роботу проектів.
- Node.js, Deno та Bun у порівнянні: результати тестувань, компроміси та стратегія міграції — Пояснюються справжні архітектурні відмінності між Node.js, Deno та Bun, що розкривають результати тестувань 2025 року, та як вирішити, чи потрібна міграція та коли її проводити.