Optymalizuj aplikacje jak mistrz
Praktyczne podejście do wydajności w Javie i backendach
Hej! Dzisiaj chcę podzielić się z Tobą doświadczeniami, które zdobyłem przez lata pracy z backendami w Javie. Nie będzie tu suchych definicji z dokumentacji – opowiem Ci o realnych problemach, które napotkałem, i jak je rozwiązywałem.
Dlaczego wydajność ma znaczenie?
Pamiętam projekt, gdzie aplikacja działała w porządku na lokalnym środowisku, ale po wdrożeniu na produkcję zaczęła się dusić przy większym ruchu. Response time skakał z 200ms do 3-4 sekund. Klienci narzekali, a ja musiałem to naprawić.
Okazało się, że problem nie był w samym kodzie, ale w tym, jak kod korzystał z zasobów. Za dużo połączeń do bazy, brak cache'owania, nieoptymalne zapytania SQL – klasyka. Ale o tym za chwilę.
Pamięć i garbage collection
Java ma garbage collector, który automatycznie zarządza pamięcią. Brzmi super, ale jeśli nie rozumiesz, jak działa, możesz mieć problemy. W jednym z projektów miałem aplikację, która co kilka godzin "zawieszała się" na 2-3 sekundy.
Po analizie okazało się, że GC robił full stop-the-world collection, bo aplikacja generowała za dużo obiektów krótkotrwałych. Rozwiązanie? Zmiana strategii GC na G1GC i optymalizacja tworzenia obiektów. Zamiast tworzyć nowe Stringi w pętlach, używałem StringBuilder. Zamiast nowych list w każdym requestcie, używałem puli obiektów.
💡 Praktyczna rada:
Jeśli widzisz w logach częste GC pauses, sprawdź profilowanie pamięci. JVisualVM lub JProfiler pokażą Ci, które obiekty żyją najdłużej i gdzie tracisz pamięć.
Baza danych – najczęstszy bottleneck
Większość problemów z wydajnością backendów wynika z bazy danych. Nie chodzi o to, że baza jest wolna – chodzi o to, jak z niej korzystamy.
W jednym projekcie miałem endpoint, który robił 15 zapytań do bazy dla jednego requesta. Każde zapytanie osobno. Response time: 800ms. Po przepisaniu na jedno zapytanie z JOINami: 120ms. Różnica jest ogromna.
Kluczowe rzeczy, które nauczyłem się robić:
- Używaj batch operations – zamiast 100 INSERTów, zrób jeden batch. Hibernate/JPA to wspiera, ale trzeba to włączyć.
- Indeksy są ważne – jeśli robisz WHERE na kolumnie bez indeksu, baza musi przeskanować całą tabelę. Sprawdź EXPLAIN PLAN.
- Connection pooling – nie otwieraj nowego połączenia dla każdego requesta. Użyj HikariCP lub podobnego. Domyślne ustawienia często są za niskie.
- Cache'owanie – jeśli dane się rzadko zmieniają, cache'uj je. Redis, Caffeine, albo nawet prosty ConcurrentHashMap z TTL.
Threading i asynchroniczność
Java ma świetne narzędzia do pracy z wątkami, ale trzeba wiedzieć, kiedy ich używać. W jednym projekcie miałem endpoint, który czekał na odpowiedzi z 5 zewnętrznych API. Każde wywołanie trwało 200-300ms, więc łącznie: ponad sekunda.
Przepisałem to na CompletableFuture z parallel execution. Teraz wszystkie wywołania idą równolegle, a czas odpowiedzi to maksimum z nich, nie suma. Z 1.2s do 300ms.
⚠️ Uwaga:
Nie przesadzaj z wątkami. Każdy wątek kosztuje pamięć. Jeśli masz 1000 requestów na sekundę i każdy tworzy wątek, szybko zabraknie Ci zasobów. Użyj thread pool z rozsądnym limitem.
Monitoring i profilowanie
Nie da się optymalizować tego, czego nie mierzysz. Zawsze ustawiam monitoring:
- APM tools – New Relic, Datadog, albo nawet prosty Prometheus + Grafana. Widzisz, które endpointy są wolne, gdzie są bottlenecki.
- Logi z czasem wykonania – w każdym ważnym miejscu loguję czas wykonania. Dzięki temu wiem, czy problem jest w bazie, w logice biznesowej, czy w zewnętrznych API.
- Profilowanie – JProfiler, YourKit, albo nawet prosty JFR (Java Flight Recorder). Pokazują Ci, gdzie spędzasz czas CPU, gdzie alokujesz pamięć.
Praktyczne przykłady z moich projektów
W projekcie, który obsługiwał 80k+ zapytań miesięcznie, miałem problem z endpointem listowania produktów. Response time rósł liniowo z liczbą produktów. Po analizie okazało się, że:
- Zapytanie SQL nie miało indeksu na kolumnie sortowania
- Dla każdego produktu robiłem osobne zapytanie o kategorię (N+1 problem)
- Nie było paginacji – zwracałem wszystkie produkty naraz
Naprawiłem to przez: dodanie indeksu, użycie JOIN zamiast osobnych zapytań, i dodanie paginacji (LIMIT/OFFSET). Response time spadł z 2s do 150ms przy 100 produktach na stronie.
Podsumowanie
Optymalizacja to nie jednorazowa akcja – to ciągły proces. Zawsze mierz, analizuj, poprawiaj. Nie optymalizuj przedwcześnie, ale też nie ignoruj problemów z wydajnością.
Najważniejsze lekcje, które wyniosłem:
- Baza danych to najczęstszy bottleneck – optymalizuj zapytania i użyj cache'owania
- Pamięć i GC – rozumiej, jak działa, i profiluj aplikację
- Threading – używaj mądrze, nie przesadzaj
- Monitoring – mierz wszystko, co ważne
Jeśli masz pytania albo chcesz podzielić się swoimi doświadczeniami, daj znać. Zawsze chętnie wymienię się wiedzą z innymi developerami.