Optymalizacja renderowania we Flutter: Jak osiągnąć 120fps?

Kluczowe Wnioski

  • Silnik Impeller eliminuje problem "janków" przy kompilacji shaderów na iOS.
  • Nadmiarowe przerysowania (rebuilds) to wróg numer jeden; używaj const i RepaintBoundary.
  • Ciężkie obliczenia (JSON parsing, krypto) zawsze przenoś do osobnych Isolates.

Flutter jest znany ze swojej wydajności, dążąc do renderowania 60 lub 120 klatek na sekundę. Jednak w skomplikowanych aplikacjach produkcyjnych łatwo o błędy, które prowadzą do gubienia klatek (jank). Ten artykuł to techniczny deep-dive w mechanizmy renderowania Fluttera.

1. Silnik Renderujący: Skia vs Impeller

Przez lata Flutter polegał na silniku Skia. Choć potężny, Skia ma wadę: shadery są kompilowane w momencie pierwszego użycia (JIT). To powodowało słynne przycięcia animacji przy pierwszym uruchomieniu aplikacji na iOS.

Rozwiązanie: Impeller. Nowy silnik napisany przez zespół Fluttera od zera. Kluczowa różnica? Wszystkie shadery są prekompilowane podczas budowania aplikacji. To oznacza, że silnik nigdy nie musi "myśleć" jak narysować kształt w trakcie animacji – on już to wie.

2. Unikanie nadmiarowych przerysowań

Metoda build() we Flutterze może być wywoływana nawet 60 razy na sekundę. Jeśli wykonujesz tam kosztowne operacje lub tworzysz nowe instancje obiektów, które mogłyby być stałe, marnujesz cykle procesora.

Używaj const

// ŹLE: Tworzy nową instancję Container przy każdym build()
        return Container(child: Text('Stały tekst'));
        
        // DOBRZE: Kompilator wie, że to się nigdy nie zmieni
        return const Container(child: Text('Stały tekst'));

RepaintBoundary

Jeśli masz małą animację (np. kręcący się spinner) wewnątrz dużej, statycznej strony, Flutter może próbować przerysować całą stronę co klatkę. Opakowanie animacji w RepaintBoundary tworzy dla niej osobną warstwę w silniku renderującym.

RepaintBoundary(
          child: CircularProgressIndicator(),
        )

3. Optymalizacja List i Obrazów

W przypadku długich list zawsze używaj ListView.builder. Konstruktor ten tworzy widgety tylko dla elementów widocznych na ekranie (viewport). Jeśli użyjesz zwykłego ListView(children: [...]), Flutter zbuduje wszystkie 1000 elementów naraz, co zamrozi UI.

Dla obrazów z sieci, biblioteka cached_network_image to standard, ale pamiętaj o parametrach memCacheWidth. Jeśli pobierasz obraz 4000x3000 pikseli, ale wyświetlasz go jako awatar 100x100, dekodowanie pełnego obrazu do pamięci RAM może spowodować crash (OOM).

4. Wielowątkowość i Isolates

Dart jest jednowątkowy. Jeśli zablokujesz główny wątek (np. parsując ogromny JSON), interfejs zamarznie. Rozwiązaniem są Isolates – niezależne wątki pamięci.

// Parsowanie JSON w tle
        Future<Map<String, dynamic>> parseJsonBg(String jsonString) async {
          return await compute(jsonDecode, jsonString);
        }
        

Funkcja compute() automatycznie tworzy Isolate, wykonuje zadanie i zwraca wynik, nie blokując UI ani na milisekundę.

Podsumowanie

Wydajność we Flutterze to suma małych decyzji architektonicznych. Świadome korzystanie z drzewa widgetów, unikanie zbędnych alokacji pamięci i delegowanie ciężkiej pracy do Isolate to klucz do płynnych, natywnych doświadczeń.