Frame Rate — to liczba klatek, którą system graficzny wyświetla w ciągu jednej sekundy. W aplikacjach mobilnych częstotliwość klatek bezpośrednio określa płynność animacji, przewijania i przejść między ekranami. Według danych Android Developers, 2025, docelowy Frame Rate wynosi 60 fps dla standardowych wyświetlaczy i 120 fps dla urządzeń z wysoką częstotliwością odświeżania. Odchylenie od docelowej wartości prowadzi do wizualnych zacięć i pogorszenia doświadczenia użytkownika.
Najważniejsze
Frame Rate (częstotliwość klatek) — to metryka mierzona w klatkach na sekundę (fps), która pokazuje, ile razy na sekundę aplikacja aktualizuje obraz na ekranie. Ludzkie oko postrzega ruch jako płynny przy częstotliwości od 24 fps (kino), ale dla interaktywnego UI wymagane jest minimum 60 fps, aby dotknięcia i animacje były odczuwalne jako natychmiastowe. Każda klatka to pełny cykl: przetwarzanie danych wejściowych użytkownika, obliczanie Layout, renderowanie hierarchii View i wyświetlenie na ekranie. Jeśli któryś z etapów przekroczy przydzielony budżet czasu (16.6 ms przy 60 fps), klatka jest pomijana, a użytkownik widzi zacięcie.
Ważne jest rozróżnienie Frame Rate aplikacji od częstotliwości odświeżania wyświetlacza (Refresh Rate). Częstotliwość odświeżania to cecha ekranu: ile razy na sekundę wyświetlacz fizycznie odświeża obraz (60, 90, 120 lub 144 Hz). Frame Rate — to ile klatek na sekundę udaje się wyrenderować aplikacji. Jeśli aplikacja generuje 60 fps przy wyświetlaczu 120 Hz, co druga klatka będzie powielana — obraz pozostanie płynny, ale nie tak responsywny, jak mógłby być. Według danych Google I/O 2023, nowoczesne flagowce są w stanie utrzymać 120 fps w prostych scenariuszach UI, ale w ciężkich (gry, złożone listy) częstotliwość spada do 40–60 fps.
Renderowanie klatki w aplikacji mobilnej przechodzi przez potok składający się z kilku etapów. W Androidzie potok obejmuje: przetwarzanie danych wejściowych (Input), animację (Animation), pomiar i rozmieszczenie (Layout), rysowanie (Draw), synchronizację z GPU i wyświetlenie na ekranie (Swap). Każdy etap jest wykonywany na CPU lub GPU, a łączny czas wszystkich etapów nie powinien przekraczać budżetu klatki. Dla 60 fps budżet wynosi 16.6 ms, dla 120 fps — 8.3 ms. Choreographer (Android) i CADisplayLink (iOS) synchronizują renderowanie z pionowym odświeżaniem wyświetlacza (VSync), gwarantując, że klatka jest wyświetlana tylko w momencie odświeżania ekranu, unikając tearingu (rozerwania obrazu).
W iOS potok jest podobny: Run Loop przetwarza zdarzenia, Core Animation oblicza warstwy, Render Server (oddzielny proces) renderuje i wysyła klatkę do GPU. Różnicą w iOS jest wydzielony proces Render Server, który izoluje renderowanie od głównej aplikacji. Jeśli aplikacja blokuje główny wątek, Render Server nadal może wyświetlić ostatnią znaną klatkę, ale animacje zatrzymają się. Jeśli natomiast Render Server sam nie nadąża — GPU marnuje czas, a Frame Rate spada. Według danych Apple WWDC 2022, najczęstszymi przyczynami niskiego Frame Rate w iOS są nadmierne zagnieżdżenie CALayer, ciężkie shadowPath i renderowanie poza ekranem (offscreen rendering).
Kod w Kotlin subskrybuje Choreographer.FrameCallback i loguje rzeczywisty czas między klatkami. Jeśli interwał przekracza 16.6 ms — rejestrowana jest pominięta klatka.
class FrameRateMonitor {
private var lastFrameTime = 0L
private val frameCallback =
Choreographer.FrameCallback { frameTimeNanos ->
if (lastFrameTime != 0L) {
val deltaMs = (frameTimeNanos - lastFrameTime) / 1_000_000f
if (deltaMs > 16.6f) {
Log.w("FrameRate",
"Skipped frame: $deltaMs ms")
}
}
lastFrameTime = frameTimeNanos
Choreographer.getInstance()
.postFrameCallback(this)
}
fun start() {
Choreographer.getInstance()
.postFrameCallback(frameCallback)
}
}
Refresh Rate (częstotliwość odświeżania) — to cecha sprzętowa wyświetlacza, określająca ile razy na sekundę ekran fizycznie przerysowuje obraz. Standardowe wyświetlacze mają 60 Hz, nowoczesne flagowce — 90, 120 lub 144 Hz. Frame Rate aplikacji może być niższy, równy lub wyższy od częstotliwości odświeżania (w ostatnim przypadku nadmiarowe klatki są odrzucane). Idealny scenariusz — Frame Rate pokrywa się z Refresh Rate: każdy cykl sprzętowy otrzymuje nową klatkę od aplikacji, a ruch jest maksymalnie płynny. Jeśli Frame Rate jest niższy, wyświetlacz powtarza ostatnią klatkę, co jest odbierane jako mikro-zacięcia (stutter).
Android i iOS obsługują dynamiczne przełączanie częstotliwości odświeżania. Android 12+ używa Smart Refresh Rate: podczas przewijania system podnosi częstotliwość do 120 Hz, przy statycznej treści obniża do 60 Hz w celu oszczędzania baterii. iOS ProMotion (iPhone 13 Pro i nowsze) działa podobnie — częstotliwość waha się od 10 do 120 Hz w zależności od treści. Deweloper powinien sprawdzać, czy urządzenie obsługuje wysoką częstotliwość, i dostosowywać budżet czasu na klatkę. Jeśli aplikacja nie nadąża wyrenderować klatki w 8.3 ms (dla 120 Hz), lepiej wymusić pracę na 60 Hz — zapewni to stabilny Frame Rate bez pominiętych klatek.
| Typ wyświetlacza | Refresh Rate | Budżet na klatkę | Urządzenia |
|---|---|---|---|
| Standardowy | 60 Hz | 16.6 ms | Większość Android/iOS |
| Wysoki | 90 Hz | 11.1 ms | OnePlus, Pixel 6+ |
| Flagowy | 120 Hz | 8.3 ms | iPhone Pro, Galaxy S22+ |
| Gamingowy | 144 Hz | 6.9 ms | ROG Phone, Nubia RedMagic |
Do pomiaru Frame Rate w aplikacjach mobilnych dostępne są zarówno wbudowane narzędzia platform, jak i zewnętrzne profilery. W Androidzie głównym narzędziem jest GPU Profiling (Developer Options → Profile GPU Rendering), które pokazuje oś czasu każdej klatki z podziałem na etapy (Draw, Prepare, Process, Execute). Bardziej szczegółową analizę zapewnia Android Studio Profiler — rejestruje on pełny profil renderowania ze wskazaniem konkretnych View powodujących przerysowanie. W iOS używa się Instruments z szablonem Core Animation — pokazuje on FPS, czas renderowania warstw i liczbę renderowań poza ekranem.
Do monitorowania Frame Rate w produkcji stosuje się Firebase Performance (Android) — zbiera on Frame Rate w tle i agreguje według urządzeń, wersji systemu operacyjnego i sesji. W iOS MetricKit dostarcza podobne dane przez MXAnimatoryMetric. Dla gier i aplikacji Flutter używa się FrameTimingCallback (Flutter) i Unity Profiler. Ważne jest mierzenie nie średniego Frame Rate, ale percentyli: P50, P90 i P99. Aplikacja może pokazywać średnie 55 fps, ale mieć P99 = 30 fps — oznacza to, że 1% czasu użytkownicy widzą silne zacięcia, co wystarcza do negatywnych opinii.
Przykład w Dart pokazuje, jak zasubskrybować FrameTimingCallback we Flutter i logować liczbę pominiętych klatek. Callback wywoływany jest po każdej zakończonej klatce.
import 'package:flutter/scheduler.dart';
class FrameRateLogger {
int totalFrames = 0;
int missedFrames = 0;
void start() {
SchedulerBinding.instance
.addTimingsCallback(_onReportTimings);
}
void _onReportTimings(List<FrameTiming> timings) {
for (final timing in timings) {
totalFrames++;
if (timing.totalSpan()
> Duration(milliseconds: 16)) {
missedFrames++;
}
}
debugPrint("FPS: \${totalFrames - missedFrames}");
}
}
Optymalizacja Frame Rate zaczyna się od identyfikacji wąskich gardeł w potoku renderowania. Na etapie Layout główne problemy to nadmierne zagnieżdżenie hierarchii View, używanie względnych Layoutów (RelativeLayout z dużą liczbą reguł) i częste wywołania requestLayout. Rozwiązanie — używanie ConstraintLayout lub płaskiej hierarchii, unikanie zagnieżdżenia powyżej 5–6 poziomów. Na etapie Draw — przerysowanie (overdraw): gdy piksel jest rysowany kilka razy na klatkę. Na przykład białe tło Activity pod półprzezroczystym fragmentem, pod którym jest kolejna warstwa — każdy piksel rysowany jest trzykrotnie. Narzędzie Debug GPU Overdraw pokazuje problematyczne strefy za pomocą oznaczeń kolorystycznych. Zaleca się utrzymywanie overdraw na poziomie 2x lub niżej.
W iOS główne problemy to ciężkie cornerRadius i masksToBounds — powodują one renderowanie poza ekranem (offscreen rendering), przy którym Core Animation tworzy tymczasowy bufor, rysuje w nim, a następnie kopiuje wynik na ekran. Offscreen rendering łatwo zauważyć w Instruments Core Animation: jeśli linia Renderer jest czerwona — występują problemy. Rozwiązanie — używanie UIImageView z wcześniej przyciętymi obrazami zamiast cornerRadius, unikanie groupOpacity i shouldRasterize bez konieczności. Dla obu platform kluczowe jest minimalizowanie liczby wywołań invalidate() i setNeedsDisplay() — każde takie wywołanie uruchamia pełny cykl przerysowania widoku.
Kod demonstruje zastąpienie głębokiego zagnieżdżenia RelativeLayout płaską strukturą ConstraintLayout. Zmniejszenie poziomu zagnieżdżenia z 4 do 1 skraca czas Layout o 30–50%.
// Przykład: płaska struktura przez ConstraintLayout
class OptimizedView(context: Context) :
ConstraintLayout(context) {
private val binding =
ItemProfileBinding.inflate(
LayoutInflater.from(context)
)
fun bind(user: User) {
binding.avatar.setImageURI(user.avatarUrl)
binding.nameText.text = user.name
// wiązanie danych bez przerysowywania całego kontenera
}
}
Nowoczesne aplikacje mobilne coraz częściej wykorzystują adaptacyjny Frame Rate — system, który dynamicznie dostosowuje docelową częstotliwość do bieżącego scenariusza. Podczas szybkiego przewijania lista wymaga 120 fps dla płynności, przy statycznym ekranie wystarczy 60 fps lub nawet 30 fps dla wideo. W Androidzie adaptacja realizowana jest przez Choreographer.setFrameInterval (API 33+) i Window.setFrameRate. Deweloper może wskazać systemowi preferowaną częstotliwość: setPreferredRefreshRate w SurfaceView lub setFrameRate w Window. iOS automatycznie zarządza częstotliwością przez ProMotion, ale deweloper może jawnie ustawiać preferredFramesPerSecond dla CADisplayLink.
Dynamiczny Frame Rate jest szczególnie ważny dla gier i aplikacji z animacjami. Według danych Google, obniżenie Frame Rate ze 120 do 60 Hz na statycznym ekranie oszczędza do 30–40% energii GPU. Aby osiągnąć najlepszą równowagę między płynnością a zużyciem energii, zaleca się: mierzyć rzeczywisty Frame Rate w różnych scenariuszach, ustawiać docelowy fps w zależności od sceny (gra — 60, menu — 30, wideo — 24) i przełączać tryby przez komponenty świadome cyklu życia, aby po zminimalizowaniu aplikacja nie marnowała zasobów na renderowanie 120 fps w tle.
Kod w Swift ustawia preferredFramesPerSecond dla CADisplayLink w iOS. Podczas przewijania częstotliwość wzrasta do 120 Hz, po zatrzymaniu — spada do 60 Hz.
class AdaptiveFrameRateManager {
private var displayLink: CADisplayLink?
func startWithHighRate() {
displayLink = CADisplayLink(
target: self,
selector: #selector(step)
)
if #available(iOS 15.0, *) {
displayLink?.preferredFrameRateRange =
CAFrameRateRange(
minimum: 60,
maximum: 120,
preferred: 120
)
}
displayLink?.add(to: .current,
forMode: .common)
}
@objc
private func step() {
// aktualizacja animacji
}
}
Często zadawane pytania
Dla aplikacji mobilnych docelowy Frame Rate to 60 fps (16.6 ms na klatkę). Dla urządzeń z wyświetlaczami 120 Hz pożądane jest 120 fps. Wartości poniżej 30 fps zauważalnie pogarszają doświadczenie użytkownika.
Frame Rate — ile klatek na sekundę renderuje aplikacja. Refresh Rate — ile razy na sekundę wyświetlacz fizycznie odświeża obraz. Gdy Frame Rate jest niższy niż Refresh Rate, wyświetlacz powiela ostatnią klatkę.
Użyj GPU Profiling w Developer Options, Android Studio Profiler lub Firebase Performance. Do programowego pomiaru — Choreographer.FrameCallback z obliczaniem interwału między klatkami.
Overdraw — przerysowanie jednego piksela kilka razy na klatkę. Każda dodatkowa warstwa zwiększa czas fazy Draw i obniża Frame Rate. Optymalny overdraw to 2x, krytyczny — 4x i więcej.
Przy statycznej treści Dynamic Frame Rate obniża częstotliwość do 30–60 Hz, zmniejszając obciążenie GPU o 30–40%. Podczas przewijania częstotliwość wzrasta do 90–120 Hz dla płynności.
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ż