„Optymalizowaliśmy dwa tygodnie, ale nie wiemy, czy jest szybciej" — to zdanie powinno nigdy nie paść w profesjonalnym zespole. Bez pomiaru optymalizacja wydajności to zgadywanie: nie wiesz, co naprawić, czy poprawka działa i kiedy wróci regresja. Ten poradnik pokazuje kompletny warsztat pomiarowy: testy obciążeniowe z k6, profilowanie kodu (Blackfire, Xdebug) i budżet wydajnościowy w CI.
Najważniejsze: Najważniejsze — mierz p50/p95/p99 czasu odpowiedzi, nie średnią. Test obciążeniowy planuj jako scenariusze (spokojny ruch, szczyt, nagły skok). Profiluj zanim zoptymalizujesz: 80% czasu zwykle zjada kilka ścieżek kodu. Wyniki zapisuj i porównuj po każdym wdrożeniu — inaczej regresje wracają niezauważone.
Dlaczego średnia kłamie — percentyle
Średni czas odpowiedzi 200 ms brzmi dobrze, dopóki nie zobaczysz rozkładu: p50 = 80 ms, p95 = 900 ms, p99 = 4 s. Średnia ukrywa ogon, a to ogon psuje doświadczenie użytkowników i SEO (Google mierzy realne czasy ładowania).
Interpretacja percentyli
- p50 (mediana) — typowe doświadczenie użytkownika.
- p95 — co piąty użytkownik czeka tyle lub dłużej; dobry cel SLA.
- p99 — najgorsze przypadki: wolne zapytania, blokady, GC, zimny cache.
Cel dla typowej aplikacji Laravel: p50 < 200 ms, p95 < 500 ms dla stron; API odpowiednio niżej. Ustal liczby zanim zaczniesz optymalizować — inaczej nie wiesz, kiedy skończyć.
k6 — testy obciążeniowe bez ciężkiego narzędzia
k6 to nowoczesny load tester (skrypt w JavaScript, jeden binarny plik). Scenariusz opisuje ruch: ile użytkowników wirtualnych (VU), jak długo i co robią.
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 20 }, // rozgrzewka
{ duration: '3m', target: 50 }, // stały ruch
{ duration: '1m', target: 150 }, // szczyt
{ duration: '2m', target: 150 }, // utrzymanie szczytu
{ duration: '1m', target: 0 }, // wygaszenie
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% żądań poniżej 500 ms
http_req_failed: ['rate<0.01'], // <1% błędów
},
};
export default function () {
const res = http.get('https://staging.example.com/orders');
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
k6 run load-test.js
Trzy scenariusze, które warto mieć pod ręką
| Scenariusz | Cel | Profil |
|---|---|---|
| Smoke | czy aplikacja w ogóle działa pod ruchem | 1–5 VU, 1 min |
| Load (średni) | wydajność przy typowym ruchu | 50–100 VU, 10 min |
| Spike | odporność na nagły skok (kampania, newsletter) | 0→300 VU w 30 s |
Wskazówka: Testuj na stagingu z kopią produkcyjnej bazy (zanonimizowaną). Test na pustej bazie deweloperskiej nie powie nic o wydajności: brak danych = brak wolnych zapytań, brak N+1, brak blokad. To najczęstszy błąd pierwszego load testu.
Co mierzyć poza k6 — metryki serwera i bazy
Load test obciąża cały stos, więc patrz szerzej
- PHP-FPM:
pm.max_children reachedw logu (limit procesów!), średnie zajęcie workerów. - Baza danych: liczba aktywnych połączeń vs limit, wolne zapytania w trakcie testu (
pg_stat_statementsprzed/po), blokady. - Redis: hit rate, zużycie pamięci, komendy na sekundę.
- Serwer: load average, CPU iowait, swap (jeśli swap rośnie — RAM na procesy FPM jest przekroczony).
Test bez patrzenia na te metryki to połowa obrazu: aplikacja może odpowiadać w 300 ms, podczas gdy baza jest o krok od wysycenia puli połączeń.
Profilowanie — gdzie naprawdę znika czas
Load test mówi „jest wolno", profil mówi „tutaj". Oto trzy narzędzia.
Blackfire — profesjonalny profiler (SaaS, darmowy plan dev): timeline wywołań, czas per funkcja, liczba zapytań, porównanie profili przed/po. Uruchamiasz na stagingu:
blackfire run php artisan route:call GET /orders # albo przez rozszerzenie w przeglądarce
Xdebug (tryb profile) — darmowy, lokalny: generuje cachegrind, który otworzysz w KCachegrind/QCachegrind. Wystarczy do znalezienia najdroższych ścieżek; nie nadaje się na produkcję (ogromny narzut).
Laravel Telescope / Pulse — zamiast profilu CPU dostajesz konkret: lista zapytań z czasami, wolne joby, wolne endpointy. Dla 90% problemów „baza vs kod" to wystarczy — szczegóły w Pulse i Telescope — monitoring.
- Praktyczna kolejność: Debugbar (co jest wolne na ekranie) → Telescope/Pulse (co jest wolne w systemie) → Blackfire/Xdebug (dlaczego ten fragment jest wolny). Profilowanie bez wcześniejszego pomiaru to strata czasu — najpierw wiadro, potem kropla.
Uwaga: Xdebug na produkcji to katastrofa wydajnościowa (narzut liczony w dziesiątkach procent). Profiluj wyłącznie na dev/stagingu. Na produkcji zostają APM-y o niskim narzucie: Pulse, Nightwatch, Blackfire (agent), Scout APM.
Budżet wydajnościowy i regresje w CI
Optymalizacja bez zabezpieczenia wróci. Warto mieć dwa mechanizmy.
1. Budżet wydajnościowy — jawna lista progów, które aplikacja musi spełniać: p95 strony listy < 500 ms, liczba zapytań na dashboard < 15, waga strony głównej < 1 MB. Budżet to część definicji „gotowe".
2. Testy regresji — najprostsza wersja to asercje w testach integracyjnych
// Test: budżet zapytań dla krytycznego ekranu
DB::enableQueryLog();
$this->get('/dashboard')->assertOk();
$this->assertLessThanOrEqual(15, count(DB::getQueryLog()));
- Cięższa wersja: k6 w pipeline na stagingu po każdym merge do main — z progami z
thresholds, które wywalają build przy regresji. Nawet prosty test „liczba zapytań ≤ N" łapie najczęstsze regresje: N+1 dodane w pull requeście.
Cykl pracy, który się nie wypala
- Zmierzyć — k6 + metryki, zapisz baseline (p50/p95/p99, RPS, liczba zapytań).
- Zdiagnozować — Debugbar → Telescope/Pulse → Blackfire.
- Naprawić jedno — największy zysk na jednostkę pracy (typowo: N+1, indeks, cache).
- Zmierzyć ponownie — ten sam scenariusz, te same metryki; porównaj z baseline.
- Zabezpieczyć — test regresji na to, co naprawiłeś.
- Powtórz od kroku 2, aż budżet jest spełniony.
Tylko ten cykl odróżnia optymalizację od rytuału. Pełną checklistę warstw (od zapytań po Octane) znajdziesz w optymalizacji wydajności aplikacji Laravel.
Najczęściej zadawane pytania
Czym się różni load testing od profilowania?
Load testing odpowiada „jak aplikacja zachowuje się pod obciążeniem" (przepustowość, p95, błędy) — testuje cały system z zewnątrz. Profilowanie odpowiada „gdzie znika czas w kodzie" — mierzy pojedyncze żądanie od środka (funkcje, zapytania). Potrzebujesz obu: load test wskaże problem, profil wskaże jego źródło.
Czy k6 może testować scenariusze z logowaniem?
Tak — k6 obsługuje POST-y, cookies, sesje i korelację tokenów: w scenariuszu logujesz użytkownika, przechwytujesz token/ciasteczko i używasz w kolejnych żądaniach. Dla aplikacji z logowaniem to konieczne, żeby testować realistyczne ścieżki (dashboard, koszyk), a nie tylko strony publiczne.
Dlaczego testuję na pustej bazie i wszystko jest szybkie?
Bo wydajność aplikacji zależy od danych: N+1 na 20 rekordach to 20 zapytań, na 100 000 — 100 000. Wolne zapytania bez indeksów na pustych tabelach nie istnieją. Testuj na stagingu z anonimizowaną kopią produkcyjnej bazy — to jedyny wiarygodny scenariusz.
Czy mogę profilować produkcję?
Xdebug — nie (zbyt duży narzut). Możesz za to używać APM-ów o niskim narzucie: Laravel Pulse, Nightwatch, agent Blackfire, Scout APM. One nie pokażą pojedynczej funkcji, ale wskażą wolne endpointy i zapytania — a to zwykle wystarcza do decyzji, co optymalizować.
Jak ustawić progi w k6, żeby były sensowne?
Od biznesu, nie od techniki: „użytkownik nie czeka dłużej niż 0,5 s" → p(95) < 500; „mniej niż 1% błędów w kampanii" → http_req_failed rate < 0.01. Progi poniżej realnych możliwości stosu demotywują; powyżej potrzeb — nie chronią. Ustaw baseline z pierwszego testu i zaostrzaj iteracyjnie.
👉 Chcesz wiedzieć, ile Twoja aplikacja naprawdę wytrzyma? Zrobimy test obciążeniowy z raportem i planem optymalizacji — opisz projekt przez formularz. Zobacz też audyt kodu legacy i utrzymanie techniczne.
📖 Zobacz również: optymalizacja wydajności aplikacji Laravel, problem N+1 w Laravel, indeksy i EXPLAIN ANALYZE oraz Pulse i Telescope — monitoring.