Przegląd wydajności
Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →
Kluczowym powodem wyboru React Native zamiast narzędzi opartych na WebView jest osiągnięcie co najmniej 60 klatek na sekundę i zapewnienie natywnego wyglądu oraz działania aplikacji. Gdzie to możliwe, React Native automatycznie optymalizuje wydajność, pozwalając skupić się na funkcjonalności bez martwienia się o wydajność. Istnieją jednak obszary, w których nie osiągnęliśmy jeszcze tego poziomu, oraz takie, gdzie React Native (podobnie jak pisanie natywnego kodu) nie może samodzielnie wybrać optymalnego podejścia. W takich przypadkach konieczna jest ręczna interwencja. Dążymy do zapewnienia płynnego działania interfejsu domyślnie, ale czasem może to być niemożliwe.
Ten przewodnik nauczy Cię podstaw diagnozowania problemów z wydajnością oraz omówi częste źródła problemów i proponowane rozwiązania.
Co musisz wiedzieć o klatkach
Pokolenie twoich dziadków nazywało filmy "ruchomymi obrazkami" nie bez powodu: realistyczny ruch wideo to iluzja tworzona przez szybkie zmiany statycznych obrazów w stałym tempie. Każdy z tych obrazów nazywamy klatką. Liczba klatek wyświetlanych na sekundę bezpośrednio wpływa na płynność i naturalność wideo (lub interfejsu). Urządzenia iOS wyświetlają co najmniej 60 klatek na sekundę, co daje tobie i systemowi UI maksymalnie 16.67 ms na wykonanie pracy potrzebnej do wygenerowania statycznego obrazu (klatki), który użytkownik zobaczy na ekranie. Jeśli nie zdążysz wykonać tej pracy w wyznaczonym czasie, nastąpi "utrata klatki", a interfejs stanie się niereaktywny.
Aby nieco skomplikować sprawę, otwórz Menu deweloperskie w swojej aplikacji i aktywuj Show Perf Monitor. Zauważysz, że istnieją dwa różne wskaźniki klatek.

Klatki JS (wątek JavaScript)
W większości aplikacji React Native logika biznesowa działa na wątku JavaScript. To właśnie tam działa aplikacja React, wykonują się zapytania API, przetwarzane są zdarzenia dotykowe itp. Aktualizacje widoków natywnych są grupowane i przesyłane na stronę natywną po każdej iteracji pętli zdarzeń, przed upływem terminu klatki (jeśli wszystko pójdzie dobrze). Jeśli wątek JavaScript nie odpowiada przez klatkę, zostanie ona uznana za utraconą. Na przykład wywołanie this.setState w głównym komponencie złożonej aplikacji może spowodować kosztowne przeliczenie poddrzew komponentów - może to zająć 200 ms i skutkować utratą 12 klatek. Wszystkie animacje sterowane przez JavaScript zamarzną na ten czas. Każda operacja dłuższa niż 100 ms będzie odczuwalna dla użytkownika.
Często dzieje się tak podczas przejść w Navigator: gdy dodajesz nową trasę, wątek JavaScript musi wyrenderować wszystkie komponenty sceny, aby wysłać odpowiednie komendy na stronę natywną do utworzenia widoków. Praca ta często zajmuje kilka klatek i powoduje jank, ponieważ przejście jest kontrolowane przez wątek JavaScript. Czasem komponenty wykonują dodatkową pracę w componentDidMount, co może skutkować drugim zacinaniem się podczas przejścia.
Innym przykładem jest reakcja na dotyk: jeśli wykonujesz intensywną pracę na wątku JavaScript przez wiele klatek, możesz zauważyć opóźnienia w reakcji TouchableOpacity. Dzieje się tak, ponieważ wątek JavaScript jest zajęty i nie może przetworzyć surowych zdarzeń dotykowych wysłanych z głównego wątku. W rezultacie TouchableOpacity nie może zareagować na dotyk i przekazać komendy do natywnego widoku, aby dostosował swoją przezroczystość.
Klatki UI (główny wątek)
Wiele osób zauważyło, że wydajność NavigatorIOS jest domyślnie lepsza niż Navigator. Powodem jest to, że animacje przejść są wykonywane całkowicie w głównym wątku, więc nie są przerywane przez utratę klatek na wątku JavaScript.
Podobnie możesz płynnie przewijać ScrollView nawet przy zablokowanym wątku JavaScript, ponieważ ScrollView działa w głównym wątku. Zdarzenia przewijania są wysyłane do wątku JS, ale ich odbiór nie jest konieczny do samego przewijania.
Częste źródła problemów z wydajnością
Tryb deweloperski (dev=true)
Wydajność wątku JavaScript znacząco spada podczas pracy w trybie deweloperskim (dev=true). Jest to nieuniknione: środowisko wykonuje znacznie więcej pracy w czasie rzeczywistym, aby zapewnić pomocne ostrzeżenia i komunikaty o błędach. Zawsze testuj wydajność w wersjach produkcyjnych.
Używanie instrukcji console.log
W spakowanej aplikacji te instrukcje mogą powodować poważne wąskie gardło w wątku JavaScript. Dotyczy to również wywołań z bibliotek debugujących takich jak redux-logger, dlatego pamiętaj o ich usunięciu przed pakowaniem. Możesz także użyć tej wtyczki Babel, która usuwa wszystkie wywołania console.*. Najpierw zainstaluj ją poleceniem npm i babel-plugin-transform-remove-console --save-dev, a następnie edytuj plik .babelrc w katalogu projektu w następujący sposób:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
To automatycznie usunie wszystkie wywołania console.* w wersjach produkcyjnych twojego projektu.
Zaleca się używanie wtyczki nawet jeśli w twoim projekcie nie ma wywołań console.*. Mogą je wykonywać również biblioteki stron trzecich.
Początkowe renderowanie ListView jest zbyt wolne lub przewijanie dużych list działa słabo
Zamiast tego użyj nowych komponentów FlatList lub SectionList. Oprócz uproszczenia API, nowe komponenty list mają znaczące ulepszenia wydajności, głównie niemal stałe zużycie pamięci niezależnie od liczby wierszy.
Jeśli twoja FlatList renderuje się powoli, upewnij się, że zaimplementowałeś getItemLayout aby zoptymalizować szybkość renderowania poprzez pominięcie pomiarów renderowanych elementów.
FPS wątku JS gwałtownie spada przy ponownym renderowaniu rzadko zmieniającego się widoku
Jeśli używasz ListView, musisz dostarczyć funkcję rowHasChanged, która może znacznie ograniczyć pracę poprzez szybkie określenie, czy wiersz wymaga ponownego renderowania. Przy użyciu niemutowalnych struktur danych wystarczy zwykłe porównanie referencji.
Podobnie możesz zaimplementować shouldComponentUpdate i określić dokładne warunki ponownego renderowania komponentu. Jeśli tworzysz czyste komponenty (gdzie wynik funkcji render zależy wyłącznie od propsów i stanu), możesz wykorzystać PureComponent, który zrobi to za ciebie. Ponownie, niemutowalne struktury danych pomagają utrzymać szybkość - jeśli musisz przeprowadzić głębokie porównanie dużej listy obiektów, ponowne renderowanie całego komponentu może być szybsze i z pewnością wymaga mniej kodu.
Spadki FPS wątku JS spowodowane intensywną pracą na wątku JavaScript
"Wolne przejścia w Navigatorze" to najczęstszy przejaw tego problemu, ale zdarzają się też inne sytuacje. Użycie InteractionManager może być dobrym rozwiązaniem, ale jeśli opóźnianie pracy podczas animacji zbyt negatywnie wpływa na doświadczenia użytkownika, warto rozważyć LayoutAnimation.
Animated API obecnie oblicza każdą klatkę kluczową na żądanie w wątku JavaScript, chyba że ustawisz useNativeDriver: true. LayoutAnimation wykorzystuje natomiast Core Animation i nie jest narażone na spadki klatek w wątku JS ani w głównym wątku.
Przykładowo użyłem tego do animowania modala (zesuwanie z góry i wytapianie półprzezroczystego nakładki) podczas inicjalizacji i odbierania odpowiedzi z kilku żądań sieciowych, renderowania zawartości modala oraz aktualizacji widoku, z którego modal został otwarty. Więcej informacji o używaniu LayoutAnimation znajdziesz w przewodniku Animacje.
Ograniczenia:
- LayoutAnimation działa tylko dla animacji typu "wystrzel i zapomnij" ("animacje statyczne") — jeśli animacja musi być przerywalna, należy użyć
Animated.
Przesuwanie widoku na ekranie (przewijanie, przesunięcie, obrót) powoduje spadki FPS w głównym wątku
Jest to szczególnie istotne, gdy masz tekst z przezroczystym tłem umieszczony na obrazie lub w każdej innej sytuacji wymagającej kompozycji alfa do przerysowywania widoku w każdej klatce. Włączenie shouldRasterizeIOS lub renderToHardwareTextureAndroid może w tym znacznie pomóc.
Uważaj, aby nie nadużywać tej funkcji, bo zużycie pamięci może gwałtownie wzrosnąć. Testuj wydajność i zużycie pamięci przy używaniu tych właściwości. Jeśli widok nie będzie już przemieszczany, wyłącz tę właściwość.
Animowanie rozmiaru obrazu powoduje spadki FPS w głównym wątku
W iOS każda zmiana szerokości lub wysokości komponentu Image powoduje ponowne przycięcie i przeskalowanie oryginalnego obrazu. To może być bardzo kosztowne, szczególnie dla dużych obrazów. Zamiast tego używaj właściwości stylu transform: [{scale}] do animowania rozmiaru. Przykładem może być powiększenie obrazu do pełnego ekranu po dotknięciu.
Mój komponent TouchableX nie reaguje płynnie
Czasem, jeśli wykonujemy akcję w tej samej klatce, w której zmieniamy przezroczystość lub podświetlenie komponentu reagującego na dotyk, efekt nie będzie widoczny aż do zakończenia funkcji onPress. Jeśli onPress wywołuje setState, które powoduje dużo pracy i utratę kilku klatek, może do tego dojść. Rozwiązaniem jest opakowanie akcji w onPress wewnątrz requestAnimationFrame:
handleOnPress() {
requestAnimationFrame(() => {
this.doExpensiveAction();
});
}
Wolne przejścia w Navigatorze
Jak wspomniano, animacje Navigator są kontrolowane przez wątek JavaScript. Wyobraź sobie przejście sceny "przesunięcie z prawej": w każdej klatce nowa scena przesuwa się z prawej do lewej, zaczynając poza ekranem (np. z przesunięciem x równym 320) i kończąc na przesunięciu x równym 0. W każdej klatce podczas przejścia wątek JavaScript musi wysłać nowe przesunięcie x do głównego wątku. Jeśli wątek JavaScript jest zablokowany, nie może tego zrobić i aktualizacja nie następuje, co powoduje zacinanie animacji.
Jednym z rozwiązań jest oddelegowanie animacji opartych na JavaScripcie do głównego wątku. Gdybyśmy w powyższym przykładzie zastosowali to podejście, moglibyśmy obliczyć listę wszystkich przesunięć x dla nowej sceny na początku przejścia i wysłać je do głównego wątku do zoptymalizowanego wykonania. Teraz, gdy wątek JavaScript jest wolny od tego zadania, utrata kilku klatek podczas renderowania sceny nie jest problemem — prawdopodobnie nawet tego nie zauważysz, bo zajmie cię piękne przejście.
Rozwiązanie tego problemu jest jednym z głównych celów nowej biblioteki React Navigation. Widoki w React Navigation używają komponentów natywnych i biblioteki Animated do dostarczania animacji z co najmniej 60 FPS, działających na wątku natywnym.