Startseite / Artikel / Trade-offs bei React Native in großem Maßstab: Zustand, Listen, Geräte, Tokens, Updates

Trade-offs bei React Native in großem Maßstab: Zustand, Listen, Geräte, Tokens, Updates

Fünf React Native-Probleme, die das ingenieurtechnische Urteilsvermögen aufzeigen: Server- versus Client-Zustand, träge FlatLists, Android-Geräte mit schwacher Leistung, Token-Speicherung und schrittweise Updates.

1719 Wörter

Das Erstellen einer Benutzeroberfläche in React Native ist einfach. Der schwierige Teil tritt auf, wenn die App wächst: Der Zustand breitet sich überall aus, Listen funktionieren unzuverlässig, günstige Android-Handys stürzen ab, Tokens werden in Klartext gespeichert und das Framework hinkt um Jahre hinterher. Im Folgenden sind fünf solcher Situationen aufgeführt, die häufig in Interviews zur Prüfung erfahrener Entwickler verwendet werden, zusammen mit den Kriterien für eine gute Antwort, damit Sie sie auf Ihre eigene Codebasis anwenden können.

Trennung des Serverzustands vom Clientzustand

Wenn lokaler Zustand, API-Antworten, Caches und gemeinsame UI-Flags zu einem unübersichtlichen Durcheinander werden, lautet die erste Frage nicht „Redux oder Context?“, sondern „Wer ist für diese Daten verantwortlich?“

Daten aus einer API, wie z. B. Profile, Feeds oder Produktlisten, gehören zur Server-Statusdaten. Sie veralten und müssen neu abgerufen, im Cache gespeichert sowie für ungültig erklärt werden. Ihr Speichern in einem globalen Store bedeutet, all das erneut manuell umzusetzen – zusätzlich zu Lade- und Fehlerflaggen. Der folgende Auszug zeigt dieses Anti-Pattern.

// ❌ Server data forced into a global store —
// now YOU own caching, staleness, invalidation, loading states…
dispatch(setProducts(await fetchProducts()));

TanStack Query und RTK Query dienen dazu, diese Schicht zu übernehmen. Hier speichert useQuery die Daten nach Kategorie und hält sie dank staleTime 60 Sekunden lang aktuell. Der zweite Teil des Blocks (im Quellcode zusammengeführt) zeigt, was für einen globalen Store übrig bleibt: ein kleines Session-Objekt.

// ✅ Server state managed by a tool built for it
const { data, refetch } = useQuery({
  queryKey: ['products', category],
  queryFn: () => fetchProducts(category),
  staleTime: 60_000,
});// ✅ Truly shared client state — small and intentional
const useSession = create<SessionStore>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

Das verbleibende Problem ist viel kleiner. Formulareingaben, Schalter und Animationswerte bleiben im Komponentenrahmen, wo sie kostengünstig sind, isoliert sind und mit dem Bildschirm verschwinden. Ein globaler Speicher sollte nur Daten enthalten, die mehrere Bildschirme benötigen und die vom Client selbst gesteuert werden, wie zum Beispiel der angemeldete Benutzer, das Designthema, Feature-Flags oder der UI-Zustand, der über mehrere Bildschirme reicht.

Was schiefgeht, wenn alles global ist

Updates führen zur Neugenerierung unverwandter Bildschirme, jede Funktion ist an die Struktur des Speichers gebunden, Tests müssen die Umgebung simulieren und Refaktorisierungen werden zu aufwendigen Aufgaben. Ein überdimensionierter Speicher sieht aus wie Architektur, verhält sich aber wie Schulden.

Diagnose eines langsamen FlatList ohne Raten

Ein FlatList wirkt träge, obwohl die API schnell ist. React.memo überall einzusetzen, bedeutet zu raten; der richtige Ansatz ist es, zuerst zu messen.

Profile mit React DevTools (oder der why-did-you-render-Bibliothek) beim Scrollen. Wenn alle Zeilen bei jedem Scrollvorgang neu gerendert werden, liegt der Grund in der referenziellen Identität: Inline-Pfeilfunktionen in renderItem, Style-Objekte, die bei jeder Renderung neu erstellt werden, oder das Fehlen eines keyExtractor, was zu erneuten Montierungen zwingt. Hier erstellt jede Renderung eine neue onPress-Klammerfunktion sowie ein neues style-Objekt, sodass keine Zeile das Rendern überspringen kann.

// ❌ New function + new object on EVERY render → every row re-renders
<FlatList
  data={items}
  renderItem={({ item }) => (
    <Row item={item} onPress={() => open(item.id)} style={{ padding: 12 }} />
  )}
/>

Die Lösung bietet React stabile Referenzen: ein gememorisierter Row, ein mit useCallback umhülltes renderItem, eine stabile Schlüsselwerte sowie getItemLayout, damit die Liste die Zeilen niemals misst. Die typisierten Props machen dies zu TSX.

// ✅ Stable identities + memoized rows
const Row = React.memo(({ item, onPress }: RowProps) => { /* … */ });const renderItem = useCallback(
  ({ item }: ListRenderItemInfo<Item>) => <Row item={item} onPress={handlePress} />,
  [handlePress],
);<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
/>

getItemLayout eignet sich nur für Zeilen mit fester Höhe; falsche Werte bei variabler Höhe verursachen Sprünge und leere Lücken.

Auswertung des Perf Monitors

Falls die Darstellung in Ordnung aussieht, aber die Frames trotzdem fehlen, vergleichen Sie die beiden Werte des Perf Monitor:

  • Niedrige JS FPS-Werte deuten darauf hin, dass der JavaScript-Thread überlastet ist – meist durch eine aufwendige renderItem-Funktion oder ein unbeschränktes onScroll.
  • Niedrige UI FPS-Werte weisen auf native Leistungsprobleme hin, in der Regel Bilder. Das Dekodieren von Vollbildfotos zu 80-Punkt-Vorlagen verschwendet Speicher und Frames; passen Sie die Größe serverseitig an oder verwenden Sie expo-image oder FastImage mit den richtigen Abmessungen.

Nur danach können Sie mit windowSize, removeClippedSubviews oder dem Wechsel zu FlashList an den Einstellungen der Liste feilen. Die Anpassung der Listeneigenschaften vor der Analyse der Zeilen behandelt lediglich das Symptom.

Krachern auf günstigen Android-Geräten vorbeugen

Eine App, die auf Flaggschiff-Handys reibungslos funktioniert, kann auf den günstigen Android-Smartphones zusammenbrechen, die die meisten Nutzer besitzen. Beginnen Sie mit Daten: Die Play Console oder Analysewerkzeuge zeigen Ihnen die tatsächliche Verteilung der Geräte, die selten mit den Handys Ihres Teams übereinstimmt.

Machen Sie Low-End-Hardware zu einem Bestandteil der täglichen Arbeit: Halten Sie ein physisches Gerät mit 2 bis 3 GB RAM in der Nähe oder nutzen Sie Firebase Test Lab für die Modelle, die aus Ihren Analysedaten hervorgehen, wie es dieser Befehl tut.

gcloud firebase test android run \
  --app app-release.apk \
  --device model=a10,version=29 \
  --device model=redmi9,version=30   # the phones in your analytics, not yours

Testen Sie immer Release-Builds. Debug-Builds verbergen die tatsächliche Leistung, und Hermes verhält sich im Release-Zustand anders als beim Debug-Modus. Typische Fehler auf schwacher Hardware sind Speichendruck durch große Bilder oder behaltene Listen, eine überlastete Hauptthread sowie Abstürze aufgrund von Speichermangel, die auf High-End-Handys nie auftreten.

Gentles Degradieren statt Absturz

Man kann die Erfahrung je nach Gerät anpassen. react-native-device-info gibt den Gesamtmemory-Bedarf an.

import DeviceInfo from 'react-native-device-info';

Mit diesem Tool kann man Geräte mit weniger als 3 GB als Low-End-Geräte kennzeichnen, stattdessen Vorschaubilder anstelle von vollständigen Bildern bereitstellen sowie Verzerrungen, Parallax-Effekte und aufwändige Animationen weglassen. Da getTotalMemory asynchron ist, sollte die Kennzeichnung beim Start einmal berechnet werden, anstatt während der Darstellung gewartet zu werden.

const totalMemory = await DeviceInfo.getTotalMemory();
const isLowEnd = totalMemory < 3 * 1024 ** 3; // < 3 GB RAM<Image
  source={{ uri: isLowEnd ? item.thumbUrl : item.fullResUrl }}
  // skip blur, parallax and heavy animations on low-end devices
/>

Veröffentlichen Sie daher vorsorglich: Segmentieren Sie die Berichte von Sentry oder Crashlytics nach Geräteklassen, nutzen Sie schrittweise Rollouts im Play Store (5%, 20%, 100%) und stoppen Sie den Prozess, bevor eine fehlerhafte Version alle Nutzer erreicht. Das Ziel ist es nicht, keine Abstürze zu haben, sondern diese früh und kostengünstig aufzufangen.

Sicheres Speichern von Authentifizierungstokens

AsyncStorage schreibt Daten unverschlüsselt auf die Festplatte: eine SQLite-Datei auf Android, einfache Sandbox-Dateien auf iOS. Ein gerootetes oder jailbroken Gerät sowie ein bösartiger Backup- oder Dateisystemzugriff machen die Tokens direkt zugänglich. Es wurde für Einstellungen und nicht für Geheimnisse entwickelt.

Tokens sollten in hardwaregestütztem Speicher gespeichert werden, wie dem iOS Keychain und dem Android Keystore, die von react-native-keychain abgebildet werden.

import * as Keychain from 'react-native-keychain';

Diese Aufrufung speichert die serialisierten Tokens mit WHEN_UNLOCKED_THIS_DEVICE_ONLY: Sie sind nur lesbar, solange das Gerät unverschlossen ist, und werden niemals auf ein anderes Gerät migriert.

await Keychain.setGenericPassword('auth', JSON.stringify(tokens), {
  accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});

Ein Aktualisierungsfluss, der konkurrierende 401-Fehler übersteht

Geben Sie den Zugriffstoken eine Gültigkeitsdauer von Minuten statt Tagen, stützen Sie sie durch einen Erneuerungstoken ab, der jedes Mal ersetzt wird, wenn er ausgetauscht wird, und zentralisieren Sie dies in einer einzigen Authentifizierungsschicht, die genau einmal erneuert wird, unabhängig davon, wie viele Anfragen gleichzeitig fehlschlagen. Ein gemeinsamer Promise macht das möglich.

let refreshing: Promise<string> | null = null;

Der Axios-Interceptor wirft alles außer 401-Fehlern erneut aus. Bei einem 401-Fehler startet ??= eine Erneuerung nur dann, wenn keine im Gange ist; dadurch warten konkurrierende Fehler auf denselben Promise. finally setzt diesen wieder zurück, und die ursprüngliche Anfrage wird mit dem neuen Zugriffstoken erneut versucht. Der Quellcode packt mehrere Anweisungen auf eine einzige Zeile. Für eine ausführlichere Erklärung siehe warum konkurrierende 401-Fehler Benutzer ausloggen und die Lösung mit der einzigen Erneuerung.

api.interceptors.response.use(undefined, async (error) => {
  if (error.response?.status !== 401) throw error;  // Concurrent 401s all await the SAME refresh — no refresh storm
  refreshing ??= refreshTokens().finally(() => (refreshing = null));
  const newToken = await refreshing;  error.config.headers.Authorization = `Bearer ${newToken}`;
  return api.request(error.config); // retry the original request
});

Zwei weitere Hinweise: Halten Sie Token nicht länger als nötig im für JavaScript zugänglichen globalen Zustand, fügen Sie für besonders sensible APIs Certificate Pinning hinzu und protokollieren Sie niemals Token, da Crash-Reporter gerne die Anfragenachrichten erfassen. „Use SecureStore“ bezeichnet ein Tool; eine fundierte Antwort umfasst die Aspekte Lebensdauer, Erneuerung und Fehlerverhalten.

Aktualisierung einer zwei Jahre alten React Native-App

Die App ist zwei Jahre veraltet, Ausfälle sind nicht zulässig und es gibt kein Budget für eine Neuschreibung.

Springen Sie niemals direkt auf die neueste Version. Der React Native Upgrade Helper zeigt den genauen Unterschied zwischen den Versionen; gehen Sie schrittweise um eine oder zwei Miniversionen vor, damit die App stets kompilier- und veröffentlichbar bleibt. Jeder Schritt stellt eine normale Veröffentlichung dar, wodurch Ausfälle vermieden werden.

Auditen Sie zunächst die Abhängigkeiten

Die Aktualisierungen betreffen alte, nicht mehr gewartete native Bibliotheken und nicht React Native selbst. Vor dem ersten Schritt sollten Abhängigkeiten identifiziert werden, die die neue Architektur oder neuere Anforderungen an Gradle und Xcode blockieren, und diese sollten ersetzt oder durch eigene Kopien ersetzt werden.

Automatisierung der Überprüfung jedes Schritts

Stellen Sie vor Beginn End-to-End-Smoke-Tests für kritische Abläufe ein, damit jeder Schritt innerhalb weniger Minuten überprüft werden kann, anstatt durch manuelle QA-Arbeit. Dieser Maestro-Flow melden sich mit einer E-Mail aus einer Umgebungsvariable an und prüft, ob Home-, Warenkorb- und Zahlungsfunktionen erreichbar sind.

# smoke-test.yaml — run with Maestro on every upgrade hop
appId: com.myapp
---
- launchApp
- tapOn: "Log in"
- inputText: ${EMAIL}
- tapOn: "Continue"
- assertVisible: "Home"
- tapOn: "Cart"
- assertVisible: "Checkout"

Päsentieren Sie die Arbeit dem Unternehmen als Risikominderung und nicht als Refactoring: Jeder verspätete Versionsschritt erhöht die Kosten für den nächsten erzwungenen Upgrade aufgrund von Store-Vorgaben, OS-Deprecierungen und Sicherheitspatches. Kleine Schritte verwandeln ein beängstigendes Projekt in eine Reihe langweiliger Veröffentlichungen – und Langeweile ist das Ziel.

Kernpunkte

  • Entscheiden Sie, wer für jeden Datensatz verantwortlich ist; lassen Sie eine Abfragenbibliothek den Serverzustand verwalten, um den globalen Speicher klein zu halten.
  • Profilieren Sie vor der Optimierung: Identitätsprobleme, Arbeit in JS-Threads sowie Bilddekodierung erfordern unterschiedliche Lösungen.
  • Testen Sie die Veröffentlichungsbuilds auf den echten Geräten Ihrer Nutzer und führen Sie sie schrittweise ein.
  • Betrachten Sie Tokens als eigenes System: sichere Speicherung, kurze Lebensdauer, Rotation sowie einmalige Aktualisierung.
  • Führen Sie Updates in kleinen, veröffentlichbaren Schritten durch, unterstützt von automatisierten Smoke-Tests.