Як виявити вузьке місце продуктивності в навантажувальному тесті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Як виявити вузьке місце продуктивності в навантажувальному тесті
Середній
~2-3 дні
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    956
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    947

Як послідовно виявити вузьке місце продуктивності

Навантажувальний тест показав падіння: latency злетіла з 50 ms до 2000 ms, а логи мовчать. Ситуація знайома — ми бачили її на десятках проектів. В одному випадку причиною виявився повільний JSON.parse в гарячому шляху: заміна на simdjson знизила p95 latency з 200 ms до 30 ms — економія на інфраструктурі склала понад 60% (близько $6000 на місяць). Підхід один: виміряти → знайти → усунути → повторити.

Діагностичний фреймворк: від метрик до вузького місця

Насамперед дивимося на метрики верхнього рівня. Якщо p95 latency висока, а CPU завантажений менше ніж 70% — шукаємо проблему в БД або зовнішніх викликах. Якщо CPU 90–100% — профілюємо код. Якщо зростає memory і йде swap — шукаємо витік. Системні помилки (ENOMEM, EMFILE) — перевіряємо ліміти ОС. Помилки 502/504 — дивимося балансувальник.

Висока latency або помилки
        │
        ├── p95 latency висока, CPU < 70%, memory OK
        │   └── → База даних: повільні запити, блокування, N+1
        │
        ├── CPU 90–100%, latency зростає пропорційно
        │   └── → Обчислювальний bottleneck: профілювати CPU-hot paths
        │
        ├── Memory зростає, swap активний
        │   └── → Витік пам'яті або heap too small
        │
        ├── ENOMEM / EMFILE / ECONNREFUSED
        │   └── → Системні ліміти: ulimit, file descriptors, TCP backlog
        │
        └── Помилки 502/504, додаток OK
            └── → Nginx upstream, load balancer timeout

Оптимізація бази даних: повільні запити та N+1

Методи виявлення повільних запитів в PostgreSQL

Під час навантажувального тесту виконуємо наступні запити. Перший покаже активні запити з тривалістю. Другий — блокування (хто кого чекає). Третій — найважчі запити за aggregate часом. Четвертий — таблиці з sequential scans (потенційні missing indexes).

-- Запущені запити прямо зараз (виконувати під час тесту)
SELECT pid, now() - query_start AS duration,
       state, wait_event_type, wait_event,
       left(query, 100) AS query_preview
FROM pg_stat_activity
WHERE state != 'idle'
  AND query NOT LIKE '%pg_stat_activity%'
ORDER BY duration DESC;

-- Блокування: хто кого блокує
SELECT blocked.pid, blocked.query,
       blocking.pid AS blocking_pid,
       blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.cardinality(pg_blocking_pids(blocked.pid)) > 0;

-- Найважчі запити (pg_stat_statements)
SELECT query, calls, mean_exec_time, total_exec_time,
       stddev_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

-- Missing indexes: sequential scans на великих таблицях
SELECT relname, seq_scan, seq_tup_read,
       idx_scan, seq_tup_read / nullif(seq_scan, 0) AS avg_rows_per_seqscan
FROM pg_stat_user_tables
WHERE seq_scan > 100
  AND seq_tup_read > 10000
ORDER BY seq_tup_read DESC;

За даними документації PostgreSQL, pg_stat_statements є стандартним розширенням для моніторингу запитів (PostgreSQL docs).

Причини деградації через N+1 запити

N+1 виникає, коли ORM для кожного батьківського об'єкта виконує окремий запит до пов'язаної таблиці. При 1000 користувачах це 1001 запит замість одного JOIN. Симптом: кількість active connections в БД дорівнює кількості віртуальних користувачів, а pg_stat_statements показує один і той самий запит з великою кількістю викликів. Рішення — eager loading, DataLoader або ручний JOIN. Використання pg_stat_statements для виявлення N+1 у 10 разів швидше за ручний аналіз логів. За даними документації k6, інструмент спеціалізується на навантажувальному тестуванні (k6 docs).

Профілювання додатку: Node.js, Python та connection pool

Профілювання CPU в Node.js

Найпростіший спосіб — увімкнути V8 profiler через сигнал. Запускаємо код під навантаженням, відправляємо kill -USR1 <pid>, через 30 секунд отримуємо cpu-profile.cpuprofile, відкриваємо в Chrome DevTools. V8 profiler дає на 50% більше детальної інформації порівняно з профілюванням на рівні застосунку.

// server.js — включити V8 profiling через сигнал
process.on('SIGUSR1', () => {
  const { Session } = require('inspector')
  const session = new Session()
  session.connect()

  session.post('Profiler.enable')
  session.post('Profiler.start')

  // Профілювати 30 секунд
  setTimeout(() => {
    session.post('Profiler.stop', (err, { profile }) => {
      require('fs').writeFileSync('./cpu-profile.cpuprofile', JSON.stringify(profile))
      console.log('CPU profile saved to cpu-profile.cpuprofile')
      session.disconnect()
    })
  }, 30000)
})

// Запустити під навантаженням: kill -USR1 <pid>
// Відкрити в Chrome DevTools → More Tools → JavaScript Profiler

Альтернатива — flamegraph через утиліту 0x. Вона збирає стектрейси та малює інтерактивний граф, де ширина смуги — час виконання. Типові знахідки: JSON.parse/stringify в hot path, bcrypt з високим cost factor, некешовані regex, синхронні файлові операції.

npm install -g 0x
0x --output-dir profile node server.js &
APP_PID=$!
k6 run tests/load/main.js
kill -USR2 $APP_PID
# Відкриється flamegraph.html

Профілювання Python під навантаженням

Для production використовуємо pyinstrument — він не потребує перезапуску та дає детальний звіт по кожному запиту. Додаємо middleware, яка вмикає профілювання за параметром ?profile=true.

from pyinstrument import Profiler
from flask import request, g

@app.before_request
def start_profiler():
    if request.args.get('profile') == 'true':
        g.profiler = Profiler()
        g.profiler.start()

@app.after_request
def stop_profiler(response):
    if hasattr(g, 'profiler'):
        g.profiler.stop()
        response.data = g.profiler.output_html()
        response.content_type = 'text/html'
    return response

# Запит з профілюванням: GET /api/posts?profile=true

Аналіз connection pool

Якщо клієнти чекають з'єднання (cl_waiting > 0 в pgBouncer), пул малий. Перевіряємо через SHOW POOLS; або SQL-запитом до pg_stat_activity.

-- PostgreSQL: статистика пулу з'єднань
SELECT datname, count(*) AS total_connections,
       count(*) FILTER (WHERE state = 'active') AS active,
       count(*) FILTER (WHERE state = 'idle') AS idle,
       count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting_lock
FROM pg_stat_activity
GROUP BY datname;

Аналіз часових рядів k6: як знайти момент деградації?

Скрипт аналізує часовий ряд p95 latency і знаходить першу хвилину, коли значення перевищило поріг (наприклад, 500 мс). Це допомагає прив'язати деградацію до конкретного моменту тесту.

Скрипт для аналізу часових рядів k6 (натисніть, щоб розгорнути)
import json
import pandas as pd

def find_degradation_point(json_results: str):
    """Знайти момент деградації за часовим рядом метрик"""
    records = []
    with open(json_results) as f:
        for line in f:
            try:
                record = json.loads(line)
                if record.get('type') == 'Point':
                    records.append({
                        'timestamp': record['data']['time'],
                        'metric': record['metric'],
                        'value': record['data']['value']
                    })
            except:
                continue
    df = pd.DataFrame(records)
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    p95_df = df[df['metric'] == 'http_req_duration'].copy()
    p95_df = p95_df.set_index('timestamp').resample('1min')['value'].quantile(0.95)
    threshold = 500
    degradation = p95_df[p95_df > threshold]
    if not degradation.empty:
        print(f"Degradation detected at: {degradation.index[0]}")
        print(f"p95 at degradation: {degradation.iloc[0]:.0f}ms")
    else:
        print("No degradation detected (all within threshold)")
    return p95_df

Інструменти та типові оптимізації

Порівняння інструментів профілювання

Інструмент Область Глибина Вплив на production
pg_stat_statements PostgreSQL Висока Немає
V8 profiler Node.js Висока Мінімальний
pyinstrument Python Середня Немає
0x Node.js Висока Вимагає перезапуску
flamegraph Загальна Висока Залежить від збирача

Типові оптимізації після аналізу

Вузьке місце Симптом Рішення
N+1 запити до БД DB active queries >> VU count DataLoader / eager loading / JOIN
Відсутній індекс SeqScan на великій таблиці CREATE INDEX CONCURRENTLY
Повільний JSON serialize CPU високий, hot path у serialize Protobuf / simdjson / msgpack
Connection pool overflow cl_waiting > 0 в pgBouncer Збільшити pool_size або додати replicas
GC паузи Spiky latency без CPU навантаження Збільшити heap, налаштувати GC flags
Блокування на таблицях wait_event = Lock в pg_stat Оптимізувати порядок операцій, NOWAIT

Що входить у результат?

  • Звіт з графіками часових рядів (latency, throughput, CPU, memory, БД-метрики)
  • Скрипти для відтворення навантаження
  • Детальний список вузьких місць з кодом та конфігами
  • Рекомендації з оптимізації (пріоритет, трудозатрати)
  • Верифікаційний тест після впровадження змін
  • Консультація з архітектури для запобігання майбутнім проблемам
  • Навчання команди (за потреби)
  • Підтримка при впровадженні
  • Сертифікат виконаних робіт

Результати аналізу та процес замовлення

Повний аналіз зі звітом та верифікацією займає 1–2 робочих дні. Вартість розраховується індивідуально залежно від складності системи. Зв'яжіться з нами — ми оцінимо ваш проект і підберемо оптимальний формат роботи. Наші інженери мають 10+ років досвіду в навантажувальному тестуванні та оптимізації, ми гарантуємо якість аналізу. Середня економія на хмарних ресурсах після наших оптимізацій становить від $3000 до $10000 на місяць.

Чому юніт-тести важливі, але не панацея?

Баг, знайдений юніт-тестом, коштує хвилини виправлення. Той самий баг у продакшені — години інциденту, компенсації та втрата довіри. На проекті інтернет-магазину помилка в розрахунку знижки пройшла ручне тестування, потрапила в прод і за 4 години обробила 37 замовлень за нульовою ціною. Автотест на граничні випадки розрахунку зловив би її при першому ж push. Оцініть свій проект — ми проведемо аудит поточного покриття і дамо рекомендації.

Jest — стандарт для JavaScript/TypeScript, але юніт-тести виправдані тільки там, де є ізольована логіка: функції трансформації, валідатори, бізнес-правила, утиліти. Тестувати React-компоненти через Jest + Testing Library правильно для поведінкових тестів: «кнопка з'являється після завантаження», «форма показує помилку при порожньому email». Снепшот-тести (toMatchSnapshot) — пастка: вони ламаються при будь-якій зміні верстки і стають шумом, який розробники оновлюють не дивлячись. Покриття коду (code coverage) — погана метрика якості: 80% coverage можна отримати тестами, які нічого не перевіряють. Coverage показує, що код виконався, а не те, що він працює правильно.

Критерій Jest Vitest
Швидкість для великих проектів Середня (Babel-трансформація) В 10–20 разів швидше (ES modules)
Інтеграція з Vite Через плагін Нативна
Монорепозиторії Вимагає конфігурації З коробки

Vitest як альтернатива Jest для Vite-проектів: в 10–20 разів швидше завдяки нативним ES modules без трансформації через Babel. Для монорепозиторіїв з тисячами тестів різниця у швидкості відчутна. Детальніше про юніт-тестування.

Як налаштувати E2E тести, які не будуть flaky?

Playwright обійшов Cypress за ключовими параметрами: нативна підтримка multi-tab, multi-origin, iframe; паралельне виконання на рівні тестів; WebKit, Firefox, Chromium з коробки; немає iframe для додатку — тести працюють в реальному браузері.

Playwright codegen записує дії та генерує тест — хороша точка старту, але згенерований код потрібно рефакторити. Локатори за text content крихкі: getByRole('button', { name: 'Оформить заказ' }) — стійкіше, ніж locator('.btn-primary').

Page Object Model — стандарт організації E2E тестів. Кожна сторінка — окремий клас з методами замість прямих локаторів. Коли кнопка переїхала з хедера в сайдбар — міняємо в одному місці, не шукаємо по всіх тестах.

Як уникнути flaky тестів? Типова проблема — flaky tests. Причини: race condition між запитом і рендером, анімації без очікування, залежність від зовнішніх API. Рішення: `page.waitForResponse()` замість `page.waitForTimeout()`, мокування зовнішніх API через `page.route()`.
// Погано
await page.click('#submit');
await page.waitForTimeout(2000);
await expect(page.locator('.success')).toBeVisible();

// Добре
await page.click('#submit');
await page.waitForResponse(resp =>
  resp.url().includes('/api/orders') && resp.status() === 201
);
await expect(page.getByRole('alert', { name: /заказ создан/i })).toBeVisible();

Наші інженери гарантують стабільність тестів у CI. Документація Playwright — основний інструмент на проектах з мільйонами користувачів.

Навантажувальне тестування з k6

k6 — інструмент для навантажувального тестування з JavaScript API. Сценарії пишуться як код, версіонуються в git, запускаються в CI. Три основних сценарії:

  • Spike test — різке зростання навантаження: 0 → 1000 користувачів за 30 секунд. Імітує запуск рекламної кампанії. Показує здатність системи реагувати на піки.
  • Soak test — стабільне навантаження на 2–4 години. Виявляє memory leaks, connection pool exhaustion, деградацію продуктивності.
  • Stress test — навантаження вище розрахункової (150–200% від очікуваного піку). Показує точку відмови та graceful degradation.

Порогові значення:

thresholds: {
  http_req_duration: ['p95<500', 'p99<1000'],
  http_req_failed: ['rate<0.01'],
}

p95 < 500ms означає: 95% запитів відповідають швидше півсекунди. Якщо поріг не виконується — k6 завершується з кодом помилки, CI-пайплайн падає.

На одному проекті інтернет-магазину ми виявили деградацію API на 4-й годині тесту: p95 зріс з 200ms до 2s через витік з'єднань. Після оптимізації клієнт заощадив значну суму на інцидентах та зайвих ресурсах. Отримайте аналогічний аудит вашого проекту — замовте навантажувальне тестування.

Як Core Web Vitals впливають на ранжування?

Google використовує Core Web Vitals у ранжуванні. Lighthouse CLI в CI-пайплайні: при кожному деплої перевіряємо, що LCP < 2.5s, CLS < 0.1, INP < 200ms. Детальніше про веб-продуктивність. Реальні проблеми, які Lighthouse знаходить:

  • Hero image без атрибутів width/height: CLS 0.35 при завантаженні.
  • JavaScript-бандл 2.1MB синхронно блокує парсинг: INP 450ms.
  • Шрифти без font-display: swap: невидимий текст до завантаження шрифту (FOIT).
  • Неоптимізований hero image 4MB: LCP 8.2s.

Lighthouse CI (lhci) зберігає історію метрик і надсилає коментар до PR з деградацією. За даними Google, 53% користувачів залишають сайт при завантаженні довше 3 секунд — наші тести запобігають таким втратам.

Піраміда тестування в проекті

Рівень Інструмент Кількість Швидкість
Юніт Vitest/Jest Багато (тисячі) <5 хв
Інтеграція Vitest + supertest Середня 5–15 хв
E2E Playwright Мало (happy path) 10–30 хв
Навантаження k6 За розкладом 30–60 хв
Продуктивність Lighthouse CI При кожному деплої 5 хв

Що входить в роботу?

  • Аудит поточного покриття та визначення критичних user flows.
  • Написання unit-тестів для ключової бізнес-логіки, інтеграційних тестів для API, E2E для сценаріїв користувача.
  • Налаштування паралельного виконання в CI (sharded workers для Playwright).
  • Навантажувальне тестування зі звітом та рекомендаціями.
  • Документація за тест-кейсами, навчання вашої команди роботі з тестами.
  • Гарантійна підтримка 1 місяць після впровадження.

Процес роботи

  1. Аналітика — аудит поточного тестування, виявлення слабких місць, визначення пріоритетів.
  2. Проектування — вибір інструментів, написання тест-плану, узгодження.
  3. Реалізація — написання тестів, інтеграція в CI.
  4. Тестування — прогін всіх рівнів, аналіз результатів, виправлення помилок.
  5. Деплой — запуск в прод, моніторинг метрик, навчання команди.

Терміни

Налаштування повного тест-пайплайна (Jest + Playwright + k6 + Lighthouse CI) з нуля: 2–4 тижні. Покриття E2E-тестами існуючого проекту (20–30 сценаріїв): 3–6 тижнів. Навантажувальне тестування зі звітом та рекомендаціями: 1–2 тижні. Вартість розраховується індивідуально після аудиту.

Готові обговорити ваш проект? Залиште заявку — ми проведемо аудит поточного тестування безкоштовно і запропонуємо план з економією до 60% часу на інциденти. Отримайте консультацію з тестування веб-додатків — напишіть нам.