Powrót do bloga
Java & Backend8 minut czytania

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:

  1. Zapytanie SQL nie miało indeksu na kolumnie sortowania
  2. Dla każdego produktu robiłem osobne zapytanie o kategorię (N+1 problem)
  3. 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.