Özgür Işık Damar
8 Min Lesezeit

React Native, der Native Driver und der Jank, den du endlich fühlen kannst

Wann useNativeDriver dir wirklich etwas bringt, wann er dich anlügt, und wie du den verlorenen Frame findest, der deinen Scanner-Bildschirm langsam wirken lässt.

react-nativeperformancemobile

Wir haben in einer Lager-App einen Barcode-Scanner-Screen, der pro Schicht etwa vierzigtausend Scans abwickelt. Das Produkt-Team meldete immer dasselbe zurück: "fühlt sich okay an, aber das Modal nach einem Scan ist irgendwie langsam." Zwei Monate lang habe ich das als "Android-Animation ist eben Android-Animation" abgetan.

Dann setzte ich mich neben eine Lagermitarbeiterin und schaute zu, wie sie 200 Pakete am Stück scannte. Am Ende war ich es, der es als langsam bezeichnete. Der Jank war real. Es war auch der Typ Jank, der im Dev-Cycle nicht auftaucht, weil man am Schreibtisch nie 200 Dinge am Stück scannt.

Hier ist, was ich über useNativeDriver, die Bridge und das tatsächliche Bewegen eines Frames gelernt habe.

Was useNativeDriver kurz tut

Animationen in React Native laufen klassisch über die JS-Bridge. Pro Frame berechnet JS den neuen Wert, schickt ihn über die Bridge zur Native-Seite, die das View aktualisiert. Ein Roundtrip pro Frame, bei 60 FPS. Die Bridge ist meistens okay — bis sie es nicht ist.

useNativeDriver: true kippt das. JS übergibt die Animationsdeklaration einmalig an die Native-Seite, die die Interpolation komplett auf dem UI-Thread laufen lässt. JS kann beschäftigt, blockiert oder am Brennen sein — die Animation läuft trotzdem mit 60 FPS.

import { Animated } from "react-native";
 
const opacity = useRef(new Animated.Value(0)).current;
 
useEffect(() => {
  Animated.timing(opacity, {
    toValue: 1,
    duration: 200,
    useNativeDriver: true, // <-- das relevante Flag
  }).start();
}, []);

Der Haken: Der Native Driver unterstützt nur eine Teilmenge der Properties. Transform, Opacity, Scroll-Position — ja. Width, Height, Background-Color, alles, was Layout auslöst — nein. Versuchst du Width mit useNativeDriver: true zu animieren, bekommst du eine Runtime-Warnung, und die Animation fällt stillschweigend auf den JS-Driver zurück.

Was ich ausgeliefert hatte (falsch)

Das Scan-Result-Modal glitt von unten hoch und blendete ein. Der ursprüngliche Code:

Animated.parallel([
  Animated.timing(translateY, {
    toValue: 0,
    duration: 250,
    useNativeDriver: true,
  }),
  Animated.timing(modalHeight, {
    toValue: 320,
    duration: 250,
    useNativeDriver: false, // Height kann nicht native getrieben werden
  }),
]).start();

Sieht okay aus. Animiert. Translate auf dem UI-Thread, Height auf dem JS-Thread, beide fertig zur gleichen Zeit. Was ist das Problem?

Das Problem: Während dieser 250 ms kämpfen jeder Scan-Callback, jeder Redux-Dispatch, jeder Barcode-Handler-Tick um denselben JS-Thread, der gerade die Height interpoliert. Der Screen verliert Frames. Der nächste Scan-Handler läuft zu spät. Der Nutzer sieht den Lag nach der Modal-Animation, nicht währenddessen.

Das ist der Jank, der in Flipper nicht auftaucht — außer du scannst 200 Stück am Stück.

Was tatsächlich half

Zwei Dinge, in Reihenfolge:

1. Hör auf, Layout zu animieren

Das Modal musste nicht wachsen. Es musste in voller Größe erscheinen und dann hochgleiten. Ich ersetzte die Height-Animation durch eine Transform von unterhalb des Screens:

const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
const opacity = useRef(new Animated.Value(0)).current;
 
useEffect(() => {
  Animated.parallel([
    Animated.timing(translateY, {
      toValue: SCREEN_HEIGHT - 320,
      duration: 220,
      useNativeDriver: true,
    }),
    Animated.timing(opacity, {
      toValue: 1,
      duration: 220,
      useNativeDriver: true,
    }),
  ]).start();
}, []);

Gleiches visuelles Ergebnis. Null JS-Thread-Arbeit während der Animation. Der Scan-Handler läuft pünktlich. Der nächste Barcode landet ohne Lag.

2. Verifiziere, dass der Driver wirklich nativ läuft

Der Fußschuss: React Native sagt dir nicht laut, wenn useNativeDriver: true stillschweigend zurückfällt. Es loggt eine Warnung in der Dev-Konsole und macht mit dem JS-Driver weiter. In Produktions-Builds ist die Warnung weg.

Ich habe eine kleine Dev-only-Assertion ergänzt, die beim Mount feuert und einen Test scheitern lässt, wenn die Animation lügt:

function assertNativeDriverWorks(value: Animated.Value, name: string) {
  if (__DEV__) {
    // Interne API — akzeptiere, dass das mit RN-Versionen brechen kann.
    const isNative = (value as unknown as {
      _animation: { _useNativeDriver: boolean } | null;
    })._animation?._useNativeDriver;
 
    if (isNative === false) {
      // eslint-disable-next-line no-console
      console.error(
        `[anim] ${name} erwartet Native Driver, fiel auf JS zurück`,
      );
    }
  }
}

Ja, es greift auf private Felder zu. Ja, es kann bei RN-Upgrades brechen. Deshalb in __DEV__ gewickelt und als Smoke Test, nicht als Runtime-Check.

Was mich überraschte

useNativeDriver: true ist nicht immer schneller als der JS-Driver. Bei sehr kurzen Animationen auf einem ruhigen Screen ist der JS-Driver okay, und der Bridging-Overhead, die Animation an Native zu übergeben, kann in Cold-Start-Metriken auftauchen. Der Native Driver kauft dir stabile 60 FPS unter Last. Hat dein Screen keine andere Arbeit, sehen beide Driver gleich aus.

Der Gewinn entsteht genau dann, wenn der JS-Thread beschäftigt ist. In unserem Fall: während eines Barcode-Scan-Sturms.

Trade-offs

Du kannst nicht immer native-driven. Wenn dein Design Height, Width, Padding — alles, was Layout auslöst — verwendet, hast du eine Wahl:

  • Animation auf Transform + Opacity umgestalten (fast immer möglich)
  • Die JS-Driver-Kosten akzeptieren (okay, wenn dein Screen ruhig ist)
  • Auf Reanimated 3 mit Shared Values und Worklets wechseln, das das gesamte Teilmengen-Problem umgeht

Wir nutzen Reanimated für alles mit Gesten. Für alles andere das eingebaute Animated mit useNativeDriver: true, weil es drei Zeilen Code sind und funktioniert.

Du brauchst nichts davon, bis du fühlen kannst, wie ein Screen unter Last langsam wird. An dem Tag ist der Fix das Simpelste in diesem Beitrag: Hör auf, Layout zu animieren.

Die Zusammenfassung

Der Native Driver ist kein magisches Flag. Er ist ein Vertrag, der sagt: "Ich verspreche, diese Animation berührt nur Transform und Opacity, damit der UI-Thread sie ohne Hilfe ausführen kann." Halte den Vertrag — der Screen bleibt geschmeidig, egal wie beschäftigt JS ist. Brichst du ihn stillschweigend, lieferst du einen Scanner aus, der bei zehn Scans okay und bei zweihundert langsam wirkt.

Die Bridge ist nicht langsam. Die Bridge ist beschäftigt mit allem anderen, was du tust. Hol die Arbeit aus der Bridge raus.

// wenn du schon hier bist