Lag w aplikacji mobilnej to zauważalne opóźnienie między działaniem użytkownika a reakcją interfejsu, które powstaje z powodu przeciążenia głównego wątku, wycieków pamięci lub nieoptymalnych operacji wejścia-wyjścia. W przeciwieństwie do błędów związanych z logicznymi pomyłkami, lag to problem wydajności: aplikacja działa poprawnie, ale wolno. Według AppDynamics Mobile App Performance Report 2024, 62% użytkowników usuwa aplikację, jeśli działa wolno dłużej niż 3 sekundy. Diagnostyka lagów wymaga profilowania CPU, pamięci i sieci za pomocą Android Studio Profiler i Xcode Instruments.
Najważniejsze
Lag (z ang. lag) w aplikacji mobilnej to subiektywnie odczuwalne opóźnienie między działaniem użytkownika (dotknięcie, przesunięcie, wprowadzenie tekstu) a reakcją interfejsu. Technicznie lag mierzy się jako czas między zdarzeniem wejścia a pełnym renderowaniem klatki: komfortowy próg — do 100 ms, zauważalny — od 200 ms, krytyczny — powyżej 500 ms.
W terminologii użytkowników „laguje” i „zwalnia” są często używane zamiennie, ale technicznie lag to stałe opóźnienie (np. 300 ms przy każdym kliknięciu), a „zwalnia” — niestałe spowolnienie: aplikacja działa płynnie, po czym zawiesza się na sekundę. Błąd, w przeciwieństwie do laga, dotyczy nie szybkości, a poprawności wyświetlania.
Google Play i App Store uwzględniają wskaźniki wydajności przy rankingu aplikacji. Wskaźnik ANR, częstotliwość jank i czas uruchamiania wpływają na widoczność w wyszukiwarce i konwersję instalacji. Aplikacja z ciągłymi lagami traci do 40% użytkowników po pierwszym uruchomieniu.
Lagi powstają, gdy główny wątek UI nie nadąża z przetwarzaniem klatek z częstotliwością 60 FPS (16.6 ms na klatkę) lub 120 FPS (8.3 ms). Rozważmy główne źródła opóźnień.
Każda synchroniczna operacja w wątku UI — odczyt z SharedPreferences, praca z bazą danych przez Room bez suspend, dekodowanie obrazu do Bitmap — blokuje renderowanie klatki. Na Androidzie prowadzi to do pomijania klatek (jank), na iOS — do opóźnienia renderowania Core Animation.
Gdy garbage collector (Garbage Collector) w Androidzie lub ARC w iOS wykonuje zwalnianie pamięci, wszystkie wątki są wstrzymywane. Częste pauzy GC powstają przy tworzeniu dużej liczby tymczasowych obiektów — na przykład przy każdym wywołaniu adaptera listy tworzona jest nowa instancja ViewHolder. Przejawia się to jako szarpane przewijanie.
Zagnieżdżone ConstraintLayout, wielokrotne LinearLayout, nakładające się View — każde zagnieżdżenie zwiększa czas measure i layout pass. Xcode wskazuje, że głęboka hierarchia warstw (ponad 10 poziomów) powoduje spadek FPS o 20-30%.
Do wykrywania przyczyn lagów używa się profilerów wbudowanych w IDE oraz narzędzi monitorowania systemowego. Każde narzędzie rozwiązuje swoje zadanie.
CPU Profiler pokazuje, które metody zajmują czas procesora i w których wątkach są wykonywane. Jeśli metoda z ciężkimi obliczeniami jest wykonywana w main thread — to źródło problemu. Nagranie śledzenia z włączonym sample Java Method pozwala zobaczyć stos wywołań w każdym momencie i znaleźć „gorące punkty”.
Analogiczne narzędzie dla iOS — Time Profiler — zbiera próbki stosu co milisekundę i pokazuje, jaki procent czasu CPU zajmuje każda metoda. Kombinacja z flagą Main Thread Only filtruje tylko operacje na głównym wątku, co bezpośrednio wskazuje źródła lagów.
Wolne zapytania sieciowe stwarzają wrażenie lagów, nawet jeśli wątek UI nie jest zablokowany. Network Profiler w Android Studio i Network Link Conditioner w Xcode pozwalają symulować wolne połączenie i sprawdzić, jak aplikacja zachowuje się w rzeczywistych warunkach. Odpowiedzi chunkowane bez postępu i duże ładunki JSON to typowe źródła pozornych lagów.
Przykład profilowania zapytania sieciowego z OkHttp z pomiarem czasu:
class TimingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val start = System.nanoTime()
val response = chain.proceed(chain.request())
val duration = (System.nanoTime() - start) / 1_000_000
Log.d("Czas", "Żądanie zajęło $duration ms")
return response
}
}
Eliminacja lagów wymaga systematycznej pracy: od optymalizacji pojedynczej metody do zmian architektonicznych. Rozważmy najskuteczniejsze techniki.
Kotlin Coroutines z dyspozytorem Dispatchers.IO dla zapytań sieciowych i Dispatchers.Default dla obliczeń gwarantują, że główny wątek pozostaje wolny dla UI. Na iOS Grand Central Dispatch z queue .global(qos: .userInitiated) dla zadań tła i .main dla aktualizacji UI to standardowe podejście. Unikaj sync-operacji między kolejkami.
RecyclerView na Androidzie i UICollectionView na iOS wymagają prawidłowej konfiguracji: ViewHolder z minimalnym tworzeniem obiektów w onBindViewHolder, DiffUtil do obliczania zmian, prefetching do wcześniejszego ładowania danych. Na iOS używaj diffable data source do animowanych aktualizacji bez ręcznego zarządzania.
Ładowanie tego samego obrazu przy każdym przewijaniu to gwarantowany lag. Coil (Android) i Kingfisher (iOS) buforują obrazy w pamięci i na dysku, zapewniając natychmiastowe wyświetlanie przy ponownym zapytaniu. Do danych używaj Room z warstwą buforującą opartą na Flow lub Combine.
Przykład konfiguracji buforowania obrazów z Coil na Androidzie:
val imageLoader = ImageLoader(context) {
memoryCachePolicy(CachePolicy.ENABLED)
diskCachePolicy(CachePolicy.ENABLED)
crossfade(true)
size(512, 512)
}
// Ładowanie z włączonym automatycznym buforowaniem
imageView.load("https://example.com/image.jpg") {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
}
Zapobieganie lagom jest tańsze niż naprawianie ich w produkcji. Środki zapobiegawcze wbudowuje się w proces rozwoju na poziomie narzędzi i architektury.
StrictMode — wbudowane narzędzie Androida, które wykrywa przypadkowe operacje wejścia-wyjścia i wywołania sieciowe na głównym wątku na etapie rozwoju. Włącz je w Application.onCreate z polityką penaltyDeath dla krytycznych naruszeń. To jedyny sposób, aby zagwarantować, że programista zobaczy problem przed commitem.
Analog dla iOS — Main Thread Checker w Xcode, wchodzący w skład Runtime Sanitization. Automatycznie sprawdza, czy wszystkie wywołania UIKit i AppKit są wykonywane z głównego wątku. Włącz go w schemacie kompilacji Debug i osiągnij zerowe ostrzeżenia w CI.
Dodaj do potoku CI uruchamianie Macrobenchmark (Android) i XCTMetrics (iOS) do pomiaru czasu uruchamiania, FPS przewijania i użycia pamięci. Ustaw progi: jeśli nowy commit zwiększa czas uruchamiania o więcej niż 5% — kompilacja upada.
Często zadawane pytania
Lag to subiektywne odczucie opóźnienia, które może występować nawet przy wysokim FPS, jeśli opóźnienie spowodowane jest czasem przetwarzania wejścia, a nie renderowania. Niski FPS (poniżej 30 klatek/s) — jedna z przyczyn lagów, ale nie jedyna.
Użyj Frame Timing API na Androidzie (Choreographer) i CADisplayLink na iOS do pomiaru czasu między klatkami. Google Play Vitals pokazuje wskaźnik jank w rzeczywistych warunkach. Do dokładnych pomiarów używaj Macrobenchmark ze scenariuszami przewijania.
Stare urządzenia mają mniej rdzeni CPU, mniejszą ilość RAM i wolniejszą pamięć. Operacja, która wykonuje się w 5 ms na flagowcu, na budżetowym urządzeniu może zająć 50 ms. Testuj wydajność na urządzeniach z niższej półki i ustaw Baseline Profiles dla AOT-kompilacji.
Tak, to jeden z najskuteczniejszych sposobów. Obrazy wysokiej rozdzielczości zajmują dużo pamięci i czasu CPU na dekodowanie. Używaj downscale do rozmiaru View, formatów WebP (Android) i HEIC (iOS), a także buforowania przez Coil lub Kingfisher.
SwiftUI automatycznie optymalizuje aktualizacje przez diffing, co zmniejsza ryzyko lagów przy zmianie danych. Jednak złożone hierarchie i częste przebudowy body mogą powodować spadek FPS. UIKit daje większą kontrolę nad wydajnością, ale wymaga ręcznej optymalizacji.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również