Strona główna / Artykuły

Od Bridge do JSI: Jak zdecydować, kiedy przeprowadzić migrację na nową architekturę React Native

Jak JSI, Fabric i TurboModules zastępują stary most React Native, co oznaczają zgłaszane korzyści w praktyce oraz jak bezpiecznie przeprowadzić audyt i migrację istniejącej aplikacji.

Wydawca: REACTAPP.TOP8 min czytania1869 słów

Źródła: React Native’s New Architecture Finally Stopped Being a Compromise

Każdy, kto wysyłał aplikację React Native o złożonej strukturze na starym typie architektury, zauważył spowolnienie przewijania w momencie, gdy wątek JavaScript zajmował się czymś innym – na przykład parsowaniem odpowiedzi z sieci lub przekazywaniem zmian stanu w dużym drzewie komponentów. To spowolnienie nie było przypadkiem; było bezpośrednią konsekwencją sposobu działania starego mostu komunikacyjnego. Nowa architektura, która go zastępuje, jest domyślna od wersji React Native 0.76, ale przez długi czas wiele zespołów traktowało ją jako coś, co należy wdrożyć później. Ten artykuł wyjaśnia, jakie dokładnie zmiany wprowadzają trzy elementy Nowej Architektury, jak interpretować informacje dotyczące jej wydajności oraz jak zdecydować, czy i w jaki sposób migrować istniejącą aplikację.

Dlaczego stary most komunikacyjny powodował spowolnienia

W architekturze tradycyjnej każda interakcja pomiędzy JavaScript a kodem natywnym odbywała się przez most asynchroniczny. Wywołanie było serializowane do formatu JSON, umieszczane w kolejce na moście, deserializowane po stronie kodu natywnego i tam wykonywane, przy czym wyniki wracały tą samą drogą. Z takiego projektu wynikają dwie kwestie: każde przekroczenie mostu wiązało się z kosztem serializacji, a ponieważ most był asynchroniczny i uporządkowany, wszystko, co miało trafić do kodu natywnego, musiało czekać, gdy wątek JavaScript był zajęty. Dlatego zajęty wątek JavaScript powodował zatrzymanie przewijania, układu oraz gestów, które od niego zależały.

Zamiennik w jakiejś formie istnieje od lat i oficjalnie jest domyślnym rozwiązaniem od wersji 0.76. Jednak bycie domyślnym i to, że ostrożny zespół postawiłby na niego w aplikacji produkcyjnej, to dwie różne kwestie. Wersje wydane w pierwszej połowie tego roku – React Native 0.85, a następnie Expo SDK 56 – są powszechnie uważane za moment, w którym ta luka została zamknięta; szczegóły znajdują się niżej.

Trzy elementy nowej architektury

Poza aspektami marketingowymi, nowa architektura składa się z trzech komponentów, które współpracują ze sobą, a każdy z nich zastępuje określoną część „mostu” łączącego poszczególne elementy.

JSI: bezpośrednie wywołania zamiast wiadomości

Interfejs JavaScript (JSI) stanowi podstawę. Jest to lekki interfejs w języku C++, za pośrednictwem którego JavaScript może przechowywać referencje do obiektów natywnych i bezpośrednio wywoływać funkcje natywne, w tym synchronicznie, gdy jest to uzasadnione. Nie ma serjalizacji JSON, żadnej kolejki mostowej ani asynchronicznego obiegu dla wywołań, które tego nie wymagają. Stary proces serjalizacji, kolejkowania, deserializacji i wysyłania zleceń sprowadza się w istocie do wywołania funkcji w języku C++.

Fabric: wspólny cień drzewa w C++

Fabric to system renderowania zbudowany na JSI. Organizuje on proces renderowania w trzy warstwy: drzewo React fiber w JavaScript, drzewo cieniowe oraz drzewo natywnych widoków hosta. Kluczowa różnica polega na tym, że drzewo cieniowe znajduje się teraz w wspólnym kodzie C++, zamiast być obsługiwane za pośrednictwem mostu od JavaScript. Układ elementów z użyciem Yoga jest obliczany w tej wspólnej warstwie, dzięki czemu informacje o układzie nie muszą już być przekazywane przez most.

Fabric jest również tym, co umożliwia działanie funkcji koncurrentnych React, takich jak przejścia, Suspense i useTransition, w React Native. Jego faza komitowania może być wykonywana synchronicznie lub równolegle, a renderer oparty na mostku nie miał sposobu na prawidłowe obsłużenie żadnego z tych trybów. Jeśli wasz zespół chce tego samego modelu renderowania koncurrentnego, który jest używany w przeglądarce, Fabric stanowi warunek wstępny, a nie element optymalizacji.

TurboModules: opóźnione, typowane moduły natywne

TurboModules zastępują starszy system NativeModules służący do dostępu do funkcjonalności natywnej. Ten fragment porównuje oba podejścia. Stary sposób importowania pobiera moduł z NativeModules, które były inicjalizowane od razu przy uruchomieniu i generowały koszty serializacji przy każdym wywołaniu. Nowy sposób importowania ładuje moduł stworzony na podstawie sprecyzowanego opisu typów, wspierany przez JSI – jest inicjalizowany dopiero przy pierwszym użyciu i może być wywoływany synchronicznie w odpowiednich sytuacjach.

// Old NativeModules: loaded eagerly at startup, whether used or not,
// with every call paying the bridge's serialization cost
import { NativeModules } from 'react-native';
const { CameraModule } = NativeModules;
// TurboModules: JSI-backed, lazily loaded only when actually called,
// with type-safe interfaces generated by Codegen from your spec
import CameraModule from './specs/NativeCameraModule';
CameraModule.capturePhoto(); // direct, synchronous-capable call via JSI

Ładowanie opóźnione ma większe znaczenie, niż się na pierwszy rzut oka wydaje. W systemach starszych aplikacje z wieloma modułami natywnymi musiały ponosić koszt inicjalizacji przy każdym uruchomieniu dla każdego z nich, nawet jeśli dana sesja w ogóle nie korzystała z większości z nich. TurboModules płacą jedynie wtedy, gdy moduł jest faktycznie używany. Drugą połową tej korzyści jest specyfikacja typowa: Codegen przekształca ją w interfejsy natywne, co jest szczegółowo omówione w naszym artykule na temat dlaczego Codegen wymaga najpierw specyfikacji dla TurboModules.

Uważne czytanie informacji o wydajności

Liczby podawane dla nowej architektury różnią się w zależności od źródła i obciążenia, więc należy je traktować jako orientacyjne wartości, a nie konkretne dane do obiecywania interesariuszom. Raportowane wyniki obejmują:

  • około 10 do 30% lepsza wydajność wątku UI dla typowych aplikacji, aż do 3 razy większa w przypadku aplikacji silnie opartych na modułach natywnych
  • 20 do 50% szybsze wywołania z JavaScriptu do kodu natywnego
  • 20 do 40% krótszy czas uruchamiania, w zależności od liczby modułów natywnych w aplikacji oraz operacji, które są mierzone

Bardziej przydatnym wskaźnikiem niż jakikolwiek procent jest to, które kategorie aplikacji przeszły z kategorii „możliwe przy użyciu workaundów” do kategorii „proste w obsłudze”. Aplikacje wymagające intensywnego wykorzystania aparatu, wizualizacja danych w czasie rzeczywistym, złożone interfejsy animowane oraz wszystko to, co wymaga rzeczywiście synchronicznego pomiaru układu, albo wymagało skomplikowanych rozwiązań architektonicznych, albo w ogóle nie były praktyczne. W ramach nowej architektury stanowią one zwykłe zadania inżynieryjne. Jeśli twoja aplikacja należy do jednej z tych kategorii, argumenty za migracją są silne, niezależnie od średnich wartości; jeśli składa się głównie z formularzy i list, korzyści będą rzeczywiste, ale mniejsze. Jedynymi liczbami, które powinny wpłynąć na twoją decyzję, są te, które zmierzasz w swojej aplikacji przed i po migracji.

Co naprawił ostatni cykl wydań

Powodem, dla którego ten rok uznawany jest za punkt zwrotny, nie jest nowa funkcjonalność, lecz zestaw poprawek dotyczących konkretnych problemów, które zmuszały ostrożne zespoły do oczekiwania. Aby uzyskać dokładne informacje, sprawdź oficjalne notatki wydawnicze, ponieważ poniższy streszczenie odzwierciedla doniesienia z chwili pisania tekstu.

  • React Native 0.85, wydany w kwietniu, przebudował system animacji współdzielonych i usunął długo istniejący problem spadków częstotliwości klatek. Podobno przejścia z intensywnymi animacjami utrzymują częstotliwość 60 klatek na sekundę na większości obsługiwanych urządzeń, zamiast pogarszać się pod obciążeniem.
  • To samo wydanie naprawiło znaczną liczbę wycieków pamięci zarówno w systemach Android, jak i iOS, które powodowały stopniowe spowolnienie długotrwałych sesji. Taki powolny regres pojawia się dopiero po długim czasie otwarcia aplikacji, co utrudnia jego wykrycie podczas standardowych testów jakości oraz diagnozowanie na podstawie zgłoszeń użytkowników o stopniowym dziwnym zachowaniu aplikacji.
  • Expo SDK 56, który ukazał się w maju, ma przyspieszyć budowanie lokalnych wersji aplikacji średniej wielkości z 110 do 120 sekund do mniej niż 50 sekund, a także skrócić czas publikacji aktualizacji bezpośrednio w urządzeniu o około jedną trzecią. Dla zespołów publikujących aplikacje codziennie oszczędności te gromadzą się szybko.
  • Integracja z React 19.2 przynosi korzyści zespołom używającym React Native Web lub aplikacji hybrydowych. Aktualizacje funkcji Suspense i Web Streams po stronie serwera umożliwiają wcześniejszą wysyłkę większej ilości elementów interfejsu z mniejszym blokowaniem głównego wątku, co pomaga części ekosystemu skierowanej do przeglądarek, której często nie docenia się w notatkach wydawniczych.
  • Migracja w praktyce: najpierw audyt zależności

    To wcale nie oznacza, że każda istniejąca aplikacja powinna zostać przekształcona już jutro. Kluczową kwestią przed dokonaniem zmian jest kompatybilność bibliotek. W chwili pisania około 85% popularnych pakietów React Native npm ma zdolność obsługi Nowej Architektury, co oznacza, że około 15% jest kompatybilnych jedynie częściowo lub wyłącznie poprzez mosty kompatybilnościowe. Migracje, które utknęły, niemal zawsze dotyczą tych pozostałych przypadków – nie samego frameworku, lecz kilku modułów natywnych od dostawców trzecich, od których zależy aplikacja.

    Wzorzec, który występuje u zespołów, które pomyślnie przeprowadzają migrację, wygląda następująco:

    • Przeprowadź audyt przed rozpoczęciem pracy. Spisz wszystkie zależności typu native i sprawdź, czy dana wersja obsługuje nową architekturę, na podstawie aktualnej wersji oprogramowania, a nie tego, co pamiętasz z zeszłego lub poprzedniego roku. Dla każdego pakietu, który nie jest obsługiwany, zdecyduj, czy go zaktualizować, zastąpić, stworzyć jego kopię lub samodzielnie go op包ować.
    • Korzystaj z Codegen. Ponieważ Codegen generuje bezpieczne pod kątem typów interfejsy typu native na podstawie Twoich specyfikacji w TypeScript lub JavaScript, cała grupa niezgodności pomiędzy kodem JavaScript a kodem natywnym jest wykrywana już podczas budowania, zamiast powodować awarie w czasie wykonywania. Jeśli piszesz własne moduły, nasze instrukcje w artykule jak połączyć w aplikacji Turbo Module z Codegen pokazują cały proces.
  • Migruj stopniowo. Zamiast dokonać jednej dużej zmiany, pracuj na gałęzi funkcjonalnej i przenoś po jednym module lub ekranie, walidując każdy krok zamiast stawiać całą aplikację na jednym wydarzeniu migracji.
  • Testuj długie sesje. Biorąc pod uwagę błędy, które zostały naprawione w najnowszych wersjach, dodaj testy trwałościowe, które utrzymują aplikację otwartą i aktywną przez dłuższy czas, oraz porównaj poziom pamięci i szybkość odświeżania obrazu przed i po ich wykonaniu.
  • Wskazówki w zależności od sytuacji

    Rozpoczynanie nowego projektu

    Nie ma większych powodów, by tego unikać. Nowa architektura jest standardem – narzędzia otoczeniowe, szczególnie Expo SDK 56, zakładają, że ją używasz – a luka kompatybilności jest teraz na tyle mała, że raczej nie napotkasz problemów, chyba że aplikacja ma nietypowe zależności natywne.

    Konserwacja aplikacji przy użyciu starej architektury

    Najpierw przeprowadź audyt kompatybilności, używając swojej rzeczywistej listy zależności, zanim zaczniesz cokolwiek innego. Udział pakietów kompatybilnych stale rośnie, więc ocena z zeszłego roku jest prawdopodobnie przestarzała. Zaplanuj migrację stopniową i traktuj błędy wykrywane przez Codegen jako rzeczywistą ochronę podczas przejścia, a nie tylko formalność.

    Zespoły, które oceniły to wcześniej i postanowiły poczekać

    Odkładanie na 2023 lub 2024 roku było rozsądną decyzją biorąc pod uwagę wówczas istniejące problemy. Poprawki dotyczące odbudowy animacji i wycieków pamięci w wersji 0.85 precyzyjnie adresują kwestie stabilności w środowisku produkcyjnym, które uzasadniały czekanie, co sprawia, że ten cykl wydań jest odpowiednim momentem na ponowne rozważenie tej kwestii.

    Główne wnioski

    • Stary most serializował każde wezwanie i umieszczał je w kolejce za zajętym wątkiem JavaScript; to, a nie pech, było przyczyną wielu problemów z wydajnością w przeszłości.
    • JSI umożliwia bezpośrednie wywołania, Fabric przenosi drzewo cieniowe i układ na wspólny kod w C++, co umożliwia równoczesne działanie React, natomiast TurboModules ładują się w trybie opóźnionym na podstawie sprecyzowanych specyfikacji.
    • Zwiększenia szybkości działania są bardzo zróżnicowane; decyduj na podstawie kategorii swojej aplikacji oraz własnych pomiarów.
    • Niedawno zmieniła się przede wszystkim stabilność i narzędzia, a nie nowe funkcje: niezawodność animacji, zachowanie pamięci podczas długich sesji oraz szybkość kompilacji.
    • Ryzyko migracji dotyczy modułów natywnych od dostawców zewnętrznych, dlatego najpierw sprawdź zależności, polegaj na Codegen i przenosz jedną ekran lub moduł po drugim.

    Literatura pokrewna

  • Jak React Query skróciło 500 wierszy z warstwy API aplikacji React Native — Przypadek studialny z rzeczywistego świata dotyczący React Native, pokazujący, jak przejście z ręcznego pobierania danych za pomocą useEffect na React Query pozwoliło usunąć powtarzający się kod oraz poprawić funkcjonowanie cache’u i obsługę pracy offline.