Startseite / Artikel

Von Bridge zu JSI: Entscheidung darüber, wann man auf die neue Architektur von React Native migriert

Wie JSI, Fabric und TurboModules die alte React Native-Brücke ersetzen, was die gemeldeten Vorteile in der Praxis bedeuten und wie man eine bestehende Anwendung sicher prüft und migriert.

Verlag: REACTAPP.TOP8 Min. Lesezeit1869 Wörter

Quellen: React Native’s New Architecture Finally Stopped Being a Compromise

Jeder, der bereits eine anspruchsvolle React Native-App unter der alten Architektur bereitgestellt hat, kennt das Problem des langsamen Scrollens, sobald der JavaScript-Thread mit anderen Aufgaben beschäftigt ist – beispielsweise beim Parsen einer Netzwerkanfrage oder bei der Übertragung von Zustandsänderungen durch einen großen Komponentenbaum. Dieses Verhalten war kein Zufall, sondern eine direkte Folge der Funktionsweise der alten Brücke. Die neue Architektur, die diese ersetzt, ist seit React Native 0.76 Standard, doch lange Zeit betrachteten viele Teams sie als etwas, das später implementiert werden sollte. Dieser Artikel erläutert, welche drei Aspekte der neuen Architektur tatsächlich geändert werden, wie man die damit verbundenen Leistungsangaben einordnet und wie man entscheidet, ob und wie eine bestehende App migriert werden soll.

Warum die alte Brücke zu Verzögerungen führte

In der herkömmlichen Architektur fand jede Interaktion zwischen JavaScript und nativem Code über eine asynchrone Brücke statt. Ein Aufruf wurde in JSON serialisiert, auf der Brücke in die Warteschlange gegeben, auf der Seite des nativen Codes wieder deserialisiert und dort ausgeführt, wobei die Ergebnisse auf dem gleichen Weg zurückgesendet wurden. Aus dieser Konzeption ergeben sich zwei Folgen: Jeder Datenaustausch verursachte Kosten durch die Serialisierung, und da die Brücke asynchron und sequenziell arbeitete, musste alles, was an den nativen Code gerichtet war, warten, solange der JavaScript-Thread belegt war. Ein beschäftigter JavaScript-Thread blockierte daher das Scrollen, die Layout-Anpassungen sowie Gesten, die von ihm abhängig waren.

Die Alternative existiert bereits seit Jahren in irgendeiner Form und ist seit Version 0.76 offiziell die Standardlösung. Allerdings sind es zwei verschiedene Dinge, die Standard zu sein und etwas zu sein, worauf ein vorsichtiger Entwicklerteam bei einer Produktanwendung setzt. Die Veröffentlichungen in der ersten Hälfte dieses Jahres, React Native 0.85 gefolgt von Expo SDK 56, gelten weithin als der Zeitpunkt, an dem diese Lücke geschlossen wurde; die Details werden weiter unten erläutert.

Die drei Bestandteile der neuen Architektur

Abschließend vom Marketing einmal abgesehen, setzt sich die neue Architektur aus drei miteinander verbundenen Komponenten zusammen, von denen jede einen bestimmten Teil der Verbindung ersetzt.

JSI: Direkte Aufrufe anstelle von Nachrichten

Die JavaScript Interface (JSI) bildet die Grundlage. Es handelt sich um eine leichte C++-Schnittstelle, über die JavaScript Verweise auf native Objekte halten und native Funktionen direkt aufrufen kann – einschließlich synchron, wenn das sinnvoll ist. Es gibt keine JSON-Serialisierung, keine Brückenwarteschlange und keinen asynchronen Hin- und Rückweg für Aufrufe, die diesen nicht benötigen. Der alte Weg der Serialisierung, Warteschlangenverwaltung, Deserialisierung und Auslösung reduziert sich im Grunde auf einen C++-Funktionsaufruf.

Fabric: ein gemeinsamer C++-Schattenbaum

Fabric ist das auf JSI basierende Rendering-System. Es strukturiert das Rendering in drei Schichten: den React-Fiber-Baum in JavaScript, einen Shadow-Tree sowie den Baum der nativen Host-Ansichten. Der entscheidende Unterschied besteht darin, dass der Shadow-Tree nun in gemeinsamem C++-Code vorhanden ist anstelle davon, über die Brücke aus JavaScript gesteuert zu werden. Die Layout-Berechnung mit Yoga erfolgt in dieser gemeinsamen Schicht, sodass Layout-Änderungen nicht mehr über die Brücke weitergeleitet werden müssen.

Fabric ist außerdem der Grund dafür, dass konkurrierende React-Funktionen wie Übergänge, Suspense und useTransition überhaupt in React Native funktionieren. Seine Commit-Phase kann synchron oder konkurrent ausgeführt werden, und der auf einer Brücke basierende Renderer hatte keine Möglichkeit, beide Modi ordnungsgemäß zu unterstützen. Wenn Ihr Team das gleiche konkurrente Rendering-Modell wie im Web nutzen möchte, ist Fabric eine Voraussetzung und keine Optimierung.

TurboModules: träge, typisierte native Module

TurboModules ersetzen das veraltete NativeModules-System zur Zugriff auf native Funktionalitäten. Der Auszug vergleicht diese beiden Ansätze. Die alte Import-Methode lädt ein Modul aus NativeModules, das bei Start sofort initialisiert wurde und bei jeder Aufrufung die Serialisierungskosten in Kauf nahm. Die neue Import-Methode lädt hingegen ein Modul, das aus einer typisierten Spezifikation unter Verwendung von JSI generiert wird, erst beim ersten Gebrauch initialisiert wird und bei Bedarf synchron aufgerufen werden kann.

// 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

Die verzögerte Ladeung ist wichtiger, als es auf den ersten Blick scheint. Im herkömmlichen System musste eine App mit vielen nativen Modulen bei jedem Start eine Initialisierungsgebühr für jedes einzelne Modul zahlen, selbst wenn in einer bestimmten Sitzung die meisten davon gar nicht genutzt wurden. TurboModules hingegen zahlen nur dann, wenn ein Modul tatsächlich verwendet wird. Die typisierte Spezifikation ist der andere Vorteil: Codegen wandelt sie in native Schnittstellen um, was ausführlich in unserem Artikel zu dem Grund, warum Codegen eine spezifikationsbasierte Vorgehensweise für TurboModules vorschreibt erläutert wird.

Aufmerksames Lesen der Leistungsangaben

Die für die neue Architektur angegebenen Zahlen unterscheiden sich je nach Quelle und Arbeitslast, daher sollten sie eher als Richtwert denn als konkrete Zahl für Stakeholder herangezogen werden. Zu den berichteten Ergebnissen gehören:

  • etwa 10 bis 30 Prozent bessere Leistung des UI-Threads bei typischen Anwendungen, bis zu 3-mal höher bei Anwendungen, die stark auf nativen Modulen beruhen
  • 20 bis 50 Prozent schnellere Aufrufe von JavaScript in nativen Code
  • 20 bis 40 Prozent kürzere Startzeiten, abhängig davon, wie viele native Module eine Anwendung hat und welche Operationen gemessen werden

Eine nützlichere Kennzahl als irgendein Prozentsatz ist die Einteilung der App-Kategorien in solche, die von „mit Workarounds möglich“ zu „einfach umsetzbar“ übergegangen sind. Apps mit starkem Fokus auf die Kamera, Echtzeit-Datenvisualisierungen, komplexe animierte Benutzeroberflächen sowie alles, was eine wirklich synchrone Maßnahme des Layouts erfordert, benötigten entweder ausgefeilte architektonische Lösungen oder waren überhaupt nicht praktikabel. In der neuen Architektur handelt es sich dabei um gewöhnliche ingenieurtechnische Aufgaben. Falls Ihre App zu einer dieser Kategorien gehört, ist der Grund für einen Umstieg unabhängig von den Durchschnittswerten stark; wenn sie hauptsächlich aus Formularen und Listen besteht, werden die Vorteile real sein, aber eher bescheiden. Die einzigen Zahlen, die Ihre Entscheidung beeinflussen sollten, sind jene, die Sie in Ihrer eigenen App vor und nach dem Umstieg messen.

Was der jüngste Release-Zyklus behoben hat

Der Grund, warum dieses Jahr als Wendepunkt angesehen wird, liegt nicht in einer neuen Funktion, sondern in einer Reihe von Korrekturen zu den spezifischen Problemen, die vorsichtige Entwicklerteams aufhielten. Für genaue Details sollten Sie die offiziellen Veröffentlichungshinweise prüfen, da der folgende Überblick Berichte zum Zeitpunkt der Erstellung widerspiegelt.

  • React Native 0.85, das im April veröffentlicht wurde, baute das gemeinsame Animationssystem neu auf und beseitigte eine seit Langem bestehende Ursache für fehlende Frames. Berichten zufolge halten animierte Übergänge auf den meisten unterstützten Geräten weiterhin 60 fps, anstatt unter Last zu leiden.
  • Die gleiche Version behebte eine beträchtliche Anzahl von Speicherverlusten sowohl auf Android als auch auf iOS, die dazu führten, dass lang laufende Sitzungen allmählich langsamer wurden. Diese Art von langsamer Leistungsabnahme tritt erst auf, nachdem eine App bereits seit längerer Zeit geöffnet ist, was es schwierig macht, sie im normalen QA-Prozess aufzudecken, sowie die Diagnose anhand von Benutzerberichten, wonach sich die App allmählich seltsam verhält, erschwert.
  • Expo SDK 56, das im Mai veröffentlicht wurde, soll die Zeit für lokale Builds mittelgroßer Apps von 110 auf 120 Sekunden auf unter 50 Sekunden reduzieren und die Zeit für das Veröffentlichen von drahtlosen Updates um etwa ein Drittel verkürzen. Für Teams, die täglich neue Versionen veröffentlichen, summieren sich diese Einsparungen schnell auf.
  • Integration von React 19.2 bringt Vorteile für Teams, die React Native Web oder hybride Anwendungen nutzen. Aktualisierungen von Server-side Suspense und Web Streams ermöglichen es, einen größeren Teil der Benutzeroberfläche früher zu übertragen, wodurch weniger die Hauptthread blockiert wird – was dem webbasierten Teil des Ökosystems hilft, den die Veröffentlichungshinweise oft zu wenig betonen.
  • Migration in der Praxis: Zuerst die Abhängigkeitsprüfung

    Das bedeutet keineswegs, dass jede bestehende Anwendung bereits morgen umgestellt werden muss. Die wichtigste Zahl, die vor dem Commit überprüft werden sollte, ist die Kompatibilität der Bibliotheken. Zum Zeitpunkt dieser Erstellung wird berichtet, dass etwa 85 % der beliebten React Native npm-Pakete die neue Architektur unterstützen, während rund 15 % nur über Brückenfunktionen verfügen oder nur teilweise kompatibel sind. Stecken gebliebene Migrationsprozesse landen fast immer in diesem Restbereich – nicht im Framework selbst, sondern in einigen Drittanbieter-Modulen, von denen die Anwendung abhängt.

    Muster, die bei Teams auftreten, die erfolgreich migrieren, sehen wie folgt aus:

    • Auditorie vor dem Start durchführen. Listet alle nativen Abhängigkeiten auf und überprüft deren Unterstützung für die neue Architektur anhand der aktuellen Version – und nicht anhand dessen, was man sich von vor einem oder zwei Jahren erinnert. Für jedes nicht unterstützte Paket entscheiden Sie, ob es aufgerüstet, ersetzt, selbst forked oder abgepackt werden soll.
    • Von Codegen profitieren. Da Codegen aus Ihren TypeScript- oder JavaScript-Spezifikationen typsichere native Schnittstellen erzeugt, werden ganze Klassen von Unstimmigkeiten zwischen JavaScript und nativem Code bereits zur Build-Zeit erkannt, anstatt als Laufzeitfehler aufzutreten. Wenn Sie eigene Module schreiben, zeigt unser Leitfaden zu der endtoend-Vernetzung eines in-App Turbo Modules mit Codegen den gesamten Ablauf.
  • Migrieren Sie schrittweise. Anstatt eines einzigen umfassenden Umstiegs arbeiten Sie an einer Feature-Branch und führen jeweils einen Modul oder ein Bildschirm um, wobei Sie jede Etappe überprüfen, anstatt das gesamte App-System auf einen einzigen Migrationsvorgang zu setzen.
  • Testen Sie lange Sitzungen. Angesichts der Fehler, die in neueren Versionen behoben wurden, sollten Sie Soak-Tests durchführen, bei denen die App über einen längeren Zeitraum geöffnet und aktiv bleibt, um den Speicherverbrauch sowie die Frame-Raten vor und nach dem Test zu vergleichen.
  • Anleitung je nach Situation

    Neues Projekt starten

    Es gibt kaum einen Grund, darauf zu verzichten. Die neue Architektur ist Standard – die zugehörigen Tools, insbesondere das Expo SDK 56, gehen davon aus, dass Sie sie verwenden – und die Kompatibilitätslücken sind inzwischen so gering, dass Sie nur dann auf Probleme stoßen werden, wenn die App ungewöhnliche native Abhängigkeiten hat.

    App auf der alten Architektur weiterbetreiben

    Führen Sie zunächst eine Kompatibilitätsprüfung durch, bevor Sie etwas anderes anfassen, wobei Sie Ihre tatsächliche Abhängigkeitsliste verwenden sollten. Der Anteil der kompatiblen Pakete steigt stetig an, sodass eine Beurteilung von vor einem Jahr vermutlich veraltet ist. Planen Sie eine schrittweise Migration und betrachten Sie die von Codegen aufgetretenen Fehler als echten Sicherheitsnetzschutz während des Übergangs und nicht nur als Formalität.

    Teams, die es früher bewertet haben und auf eine Entscheidung warten

    Angesichts der damaligen Unreinheiten war eine Verschiebung auf 2023 oder 2024 eine vernünftige Entscheidung. Die Überarbeitung der Animation sowie die Behebung von Speicherverlusten in Version 0.85 beziehen sich genau auf die Probleme bezüglich der Produktstabilität, die das Warten rechtfertigten, wodurch dieser Release-Zyklus der natürliche Zeitpunkt ist, die Frage erneut zu stellen.

    Haupterkenntnisse

    • Die herkömmliche Brücke serialisierte jeden Aufruf und stellte ihn in eine Warteschlange hinter einen stark belasteten JavaScript-Thread; daraus, und nicht aus Pech, resultierten viele der historischen Probleme mit Leistungseinbußen.
    • JSI ermöglicht direkte Aufrufe, Fabric verlagert den Schattenbaum sowie die Layout-Verarbeitung in gemeinsam genutztes C++ und ermöglicht gleichzeitige React-Ausführungen, während TurboModules verschoben aus typisierten Spezifikationen geladen werden.
    • Die gemeldeten Geschwindigkeitsverbesserungen variieren stark; entscheiden Sie sich auf der Grundlage der Kategorie Ihrer Anwendung sowie eigener Messungen.
    • Was kürzlich geändert wurde, sind Stabilität und Werkzeuge, nicht neue Funktionen: Zuverlässigkeit von Animationen, Verhalten des Arbeitsspeichers bei langen Sitzungen sowie Build-Geschwindigkeit.
    • Das Migrationsrisiko liegt bei Drittanbieter-Native-Modulen, daher prüfen Sie zunächst die Abhängigkeiten, verlassen Sie sich auf Codegen und führen Sie den Umstieg Schritt für Schritt mit einem Bildschirm oder Modul nach dem anderen durch.