FPS (Frames Per Second) — to metryka pokazująca, ile pojedynczych klatek system graficzny renderuje w ciągu jednej sekundy. W programowaniu mobilnym FPS jest standardowym wskaźnikiem wydajności UI: im wyższy FPS, tym płynniejsze animacje i bardziej responsywny interfejs. Według danych Google Android Performance, 2025, docelowa wartość FPS dla aplikacji mobilnych wynosi 60 klatek na sekundę — to próg, przy którym ludzkie oko postrzega ruch jako ciągły i płynny.
Najważniejsze
FPS (Frames Per Second) — to jednostka miary częstotliwości klatek używana w grafice komputerowej, wideo i interfejsach mobilnych. Każda klatka to statyczny obraz wyświetlany na ekranie przez krótki okres czasu. Przy szybkiej zmianie klatek mózg odbiera je jako ciągły ruch — ten efekt nazywa się persystencją wzroku. Dla aplikacji mobilnych FPS to krytyczna metryka, ponieważ każda pominięta klatka (drop) zamienia płynną animację w zauważalne zacięcie. Aplikacja musi zdążyć wyrenderować każdą klatkę ściśle w ramach budżetu czasowego: 16.6 ms dla 60 FPS, 11.1 ms dla 90 FPS, 8.3 ms dla 120 FPS.
FPS mierzy się nie tylko dla UI, ale także dla gier, wideo i aparatu. W grach FPS zależy od złożoności sceny, jakości tekstur i mocy GPU. W wideo FPS jest stały (24, 30, 60 klatek/s) i zdefiniowany przez treść. W aplikacjach mobilnych FPS zależy od efektywności kodu UI: złożoności Layout, liczby View, częstotliwości przerysowań i działania GC (Garbage Collection). Według Apple WWDC 2022 średni FPS w aplikacji może spaść o 10–15% z powodu nieefektywnego aktualizowania kolekcji (reloadData zamiast insert/delete/dequeueReusableCell). Pomiar FPS w czasie rzeczywistym to standardowa praktyka dla inżynierów QA i programistów pracujących nad wydajnością.
Obliczanie FPS w aplikacji mobilnej opiera się na pomiarze czasu między kolejnymi klatkami. Najprostszy wzór: FPS = 1000 / deltaTimeMs, gdzie deltaTimeMs to odstęp między zakończeniem poprzedniej klatki a zakończeniem bieżącej. Jeśli bieżąca klatka została wyrenderowana w 20 ms, FPS = 1000 / 20 = 50. Jednak w praktyce FPS rzadko bywa stabilny nawet w ciągu jednej sekundy: typowy profil obejmuje klatki po 12–16 ms, przeplatane pominiętymi (jank) lub wolnymi klatkami (40–60 ms). Dlatego FPS mierzy się jako średnią ruchomą z 1–5 sekund lub jako percentyle rozkładu czasu klatek.
W Androidzie FPS jest obliczany przez Choreographer, który otrzymuje callback z VSync (impuls synchronizacji wyświetlacza). Każdy callback odpowiada jednej klatce. Jeśli callback nie nadszedł — klatka została pominięta. Choreographer pozwala zmierzyć dokładną liczbę klatek na sekundę i liczbę pominiętych (skipped frames). W iOS CADisplayLink działa analogicznie — jest wywoływany za każdym razem, gdy wyświetlacz jest gotowy do renderowania nowej klatki. Właściwość timestamp zawiera dokładny czas ostatniej klatki, a targetTimestamp — oczekiwany czas następnej. Różnica między nimi to budżet czasu na bieżącą klatkę.
Kod w Swift demonstruje proste monitorowanie FPS przez CADisplayLink. Licznik frameCount zwiększa się przy każdym wywołaniu i raz na sekundę obliczany jest rzeczywisty FPS.
class FpsCounter {
private var displayLink: CADisplayLink?
private var frameCount = 0
private var lastTime = TimeInterval(0)
func start() {
displayLink = CADisplayLink(
target: self,
selector: #selector(countFrame)
)
displayLink?.add(to: .current,
forMode: .common)
}
@objc
private func countFrame() {
frameCount += 1
let now = Date().timeIntervalSince1970
if now - lastTime >= 1.0 {
print("FPS: \(frameCount)")
frameCount = 0
lastTime = now
}
}
}
Standard 60 FPS (60 Hz) utrwalił się w branży z kilku powodów. Pierwszy — fizjologiczny: ludzkie oko nie rozróżnia pojedynczych klatek przy częstotliwości powyżej 50–60 Hz, odbierając je jako płynny ruch. Ten próg nazywa się Critical Flicker Fusion (CFF). Drugi — historyczny: pierwsze lampy katodowe (CRT) działały z częstotliwością 60 Hz w USA (NTSC) i 50 Hz w Europie (PAL). Nowoczesne wyświetlacze LCD odziedziczyły tę częstotliwość. Trzeci — inżynieryjny: dla animacji UI 60 FPS zapewnia submilisekundowe opóźnienie reakcji na dotyk, co jest krytyczne dla wprowadzania tekstu, przewijania i przeciągania.
Dla programistów mobilnych 60 FPS to nie tylko zalecenie, ale ścisły budżet 16.6 ms na klatkę. Budżet ten dzieli się między wszystkie fazy renderowania: Input (1–2 ms), Animation (2–3 ms), Layout (3–5 ms), Draw (3–5 ms) i Swap (1–2 ms). Jeśli któraś faza przekroczy swój pod-budżet, klatka może nie zmieścić się w 16.6 ms. Google Android Performance zaleca zmieścić się w 12–14 ms na przygotowanie klatki, pozostawiając 2–4 ms zapasu na przerwania systemowe (GC, wątki tła). Według Firebase Performance, aplikacje ze średnim FPS poniżej 52 i P99 FPS poniżej 30 otrzymują o 35% więcej skarg na wydajność w recenzjach Google Play.
FPS i Frame Time (czas klatki) — to dwie strony tej samej metryki i ważne jest, aby ich nie mylić. FPS to prędkość, Frame Time to opóźnienie. Przy 60 FPS każda klatka zajmuje 16.6 ms. Przy 30 FPS — 33.3 ms. Ale FPS to metryka nieliniowa: spadek z 60 do 30 FPS oznacza, że czas klatki wzrósł 2 razy, a spadek z 30 do 20 — 1.5 raza. Dlatego profilery pokazują nie FPS, a Frame Time — pozwala to zobaczyć problematyczne klatki, a nie uśrednioną częstotliwość. Na przykład średnie 55 FPS może ukrywać, że 5% klatek ma Frame Time 50–100 ms — te klatki powodują Jank, ale nie wpływają znacząco na średnie FPS.
Przy analizie wydajności zaleca się patrzeć nie na średni FPS, ale na histogram Frame Time. W Android Studio Profiler i iOS Instruments Frame Time jest wyświetlany jako skala, gdzie zielona strefa to do 16.6 ms (60 FPS), żółta — 16.6–33.3 ms (30–60 FPS), czerwona — ponad 33.3 ms (poniżej 30 FPS). Każda czerwona kolumna to zauważalne dla użytkownika opóźnienie. Praktyczna zasada: P95 Frame Time (95% klatek mieści się w X ms) — to bardziej wiarygodna metryka niż średni FPS. Jeśli P95 Frame Time przekracza 32 ms (30 FPS), aplikacja jest odbierana jako wolna nawet przy średnim FPS = 50.
Funkcja w Kotlin do konwersji tablicy czasów klatek na FPS z percentylami. Zwraca nie tylko średni FPS, ale także P50, P90 i P99 do szczegółowej analizy.
data class FpsReport(
val average: Float,
val p50: Float,
val p90: Float,
val p99: Float
)
fun List<Long>.toFpsReport(): FpsReport {
val fpsValues = this.map { ms ->
if (ms > 0) 1000f / ms else 0f
}.sorted()
return FpsReport(
average = fpsValues.average().toFloat(),
p50 = fpsValues[fpsValues.size / 2],
p90 = fpsValues[(fpsValues.size * 90 / 100)],
p99 = fpsValues[(fpsValues.size * 99 / 100)]
)
}
Nowoczesne urządzenia mobilne z wyświetlaczami 90, 120 i 144 Hz stawiają nowe wymagania wobec FPS. Jeśli aplikacja generuje 60 FPS na 120-hercowym wyświetlaczu, użytkownik widzi mikro-zacinki, ponieważ co drugi cykl odświeżania ekranu otrzymuje tę samą klatkę. Aby utrzymać 120 FPS, budżet na klatkę zmniejsza się z 16.6 do 8.3 ms — wymaga to dwukrotnie wydajniejszego kodu renderowania. Według programistów Androida (Google I/O 2023), aby osiągnąć stabilne 120 FPS, należy: unikać alokacji w pętli Draw, minimalizować liczbę View w hierarchii (poniżej 80), zrezygnować z ciężkich drawable na rzecz VectorDrawable i używać surfaceView do złożonej grafiki.
W iOS sytuacja jest analogiczna: iPhone Pro z ProMotion (120 Hz) wymaga dwukrotnie więcej klatek, ale czas na każdą klatkę jest dwukrotnie krótszy. Apple zauważa, że nie wszystkie animacje muszą działać z 120 FPS — Core Animation automatycznie obniża częstotliwość dla nieruchomych lub wolno zmieniających się elementów. Jednak przewijanie, animacje gestów i przejścia powinny generować 120 FPS dla wrażenia "jedwabistości". Główne problemy przy przejściu z 60 na 120 FPS: wzrost zużycia energii (o 25–40% dla GPU), nagrzewanie się urządzenia i throttling — gdy częstotliwość spada z powodu przegrzania. Zaleca się zaimplementowanie mechanizmu fallback: jeśli Frame Time stabilnie przekracza 8.3 ms, programowo obniżyć docelową częstotliwość do 60 FPS, zamiast czekać na throttling systemowy.
Kod w Java dla Androida określa, czy urządzenie może obsługiwać 120 FPS, i przełącza tryb renderowania. Używa Display.getMode do określenia obsługiwanych częstotliwości.
class FpsModeSwitcher {
static boolean canDo120Fps(Activity activity) {
Display display = activity.getWindowManager()
.getDefaultDisplay();
for (Display.Mode mode : display.getSupportedModes()) {
if (mode.getRefreshRate() >= 120f) {
return true;
}
}
return false;
}
}
Optymalizacja FPS wymaga systematycznego podejścia, zaczynając od profilowania, a kończąc na refaktoryzacji problematycznych miejsc. Pierwszy etap — zmierzyć bieżący FPS za pomocą profilera (. Drugi etap — znaleźć klatki przekraczające budżet. Dla Androida można to zrobić przez GPU Profiling lub Perfetto. Dla iOS — Instruments z szablonem Core Animation. Trzeci etap — wyeliminować przyczyny: zmniejszyć overdraw, zmniejszyć głębokość zagnieżdżenia View, zastąpić fazę Layout przez ConstraintLayout, dodać ViewHolder Recycling, przenieść ciężkie obliczenia do wątku tła.
Specyficzne dla FPS optymalizacje obejmują: Frame Pacing — mechanizm, który równomiernie rozkłada czas między klatkami, aby uniknąć "paczek" szybkich i wolnych klatek. W Androidzie Choreographer.FrameCallback ze stałym interwałem pozwala zaimplementować Frame Pacing. W iOS CADisplayLink.preferredFrameRateRange robi to samo. Drugi mechanizm — Triple Buffering: system używa trzech buforów zamiast dwóch, co pozwala GPU rozpocząć rysowanie następnej klatki bez czekania na zwolnienie poprzedniego. Android automatycznie włącza Triple Buffering w razie potrzeby, ale w iOS programista może jawnie o to poprosić przez CAMetalLayer. Trzeci — Texture Caching: buforowanie bitmap w pamięci GPU, aby nie ładować ich ponownie przy każdej klatce.
Przykład w Kotlin demonstruje implementację Frame Pacing ze stałym interwałem 16.6 ms. Wszystkie callbacki przychodzą z równomiernym interwałem, nawet jeśli system się opóźnia.
class PacedFrameRenderer {
private val targetDelta = 16_666_666L // 16.6 ms (60 FPS)
private var lastFrameTime = 0L
private val frameCallback =
Choreographer.FrameCallback { frameTimeNanos ->
val delta = frameTimeNanos - lastFrameTime
if (delta >= targetDelta) {
onFrame(delta)
lastFrameTime = frameTimeNanos
}
Choreographer.getInstance()
.postFrameCallback(this)
}
private fun onFrame(delta: Long) {
// renderowanie klatki
}
}
Często zadawane pytania
60 FPS — komfortowy poziom dla aplikacji mobilnych. Różnica między 60 a 120 FPS jest zauważalna tylko na wyświetlaczach z wysoką częstotliwością odświeżania przy szybkich animacjach (przewijanie, przeciąganie). Poniżej 30 FPS — dyskomfort.
FPS = 1000 / FrameTime (ms). Jeśli Frame Time = 16.6 ms, FPS = 60. Jeśli Frame Time = 33.3 ms, FPS = 30. Zaleca się monitorować Frame Time, a nie FPS, ponieważ pokazuje on problematyczne klatki.
Podczas przewijania system wywołuje Layout i Draw dla każdego nowego elementu listy. Jeśli View są złożone, Layout nie jest buforowany lub używane są ciężkie drawable — Frame Time rośnie i FPS spada. Rozwiązanie — ViewHolder recycling i płaska hierarchia.
Użyj Instruments z szablonem Core Animation (pokazuje FPS w czasie rzeczywistym). Do pomiaru programowego — CADisplayLink z zliczaniem klatek na sekundę. Do produkcji — MetricKit z metryką MXAnimatoryMetric.
Triple Buffering używa trzech buforów zamiast dwóch, pozwalając GPU rozpocząć renderowanie następnej klatki przed zakończeniem VSync bieżącej. Wygładza to szczyty obciążenia i zwiększa stabilność FPS, ale dodaje 1 klatkę opóźnienia.
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ż