Startseite / Artikel

Was der React Compiler optimiert und was er Ihnen überlässt

Erfahren Sie, welche Leistungsaspekte der React Compiler automatisiert, warum langsame APIs und große Bundle weiterhin Ihre Aufgabe sind, und wie Sie ihn sicher in einer bestehenden React-Codebasis einsetzen können.

Verlag: REACTAPP.TOP6 Min. Lesezeit1421 Wörter

Quellen: React Compiler Won’t Fix a Slow React App

Der React Compiler beseitigt einen Großteil der manuellen Memoisierung, was dazu verleitet, ihn als Lösung für langsame Anwendungen zu betrachten. Das ist er jedoch nicht: Er optimiert nur die Render-Arbeit, die React steuert, und nichts anderes. Unten sehen Sie, wo diese Grenze liegt, welche Verlangsamungen weiterhin einen Ingenieur erfordern und wie der Compiler in einer bestehenden Codebasis ohne Rückfälle aktiviert werden kann.

Von einer Memoisierungs-Checkliste zur automatischen Memoisierung

Lange Zeit beschränkten sich die Ratschläge zur React-Leistung auf eine kurze Liste von Werkzeugen:

useMemo()
useCallback()
React.memo()

Ein Komponente wurde zu oft neu gerendert, also umhüllten Sie sie. Eine Berechnung schien aufwändig zu sein, also speicherten Sie sie im Cache. Eine Callback-Funktion wurde an ein Kind weitergeleitet, also stabilisierten Sie sie. Jede Lösung bestand aus manueller Memoisierung, ergänzt durch ein Abhängigkeitsarray zur Verwaltung.

Der React Compiler ändert diese Standardeinstellung. Version 1.0 gilt als stabil, und die Empfehlung des React-Teams für neuen Code lautet, dem Compiler die Memoisierung zu überlassen, anstatt useMemo und useCallback manuell einzubauen. Prüfen Sie die aktuellen React-Dokumente für aktuelle Empfehlungen zur Anwendung.

Der Haken lässt sich in einem Satz zusammenfassen: der Compiler kann die eigene Arbeit von React vereinfachen, hat aber keinen Einfluss auf Arbeiten, die außerhalb von React stattfinden.

Was der Compiler tatsächlich tut

Zur Kompilierzeit analysiert der Compiler Komponenten und Hooks und fügt an den Stellen, an denen die Analyse zeigt, dass es sinnvoll ist, Memoisierung ein. Nehmen wir eine Komponente, die eine Produktliste vor dem Rendern filtert:

function ProductList({ products }) {
  const visible = products.filter(
    product => product.inStock
  );

Der gefilterte Array wird anschließend direkt an eine Kindkomponente weitergegeben:

  return <List items={visible} />;
}

Vor dem Compiler würde ein auf Leistung fokussierter Entwickler oft den Filter explizit cachen, damit List dieselbe Array-Referenz erhält, solange sich products nicht ändert:

const visible = useMemo(
  () => products.filter(
    product => product.inStock
  ),
  [products]
);

Mit aktiviertem Compiler wird dieser Code in der Regel übersprungen. Der Compiler ermittelt die Abhängigkeiten selbst, kann präziser memorisieren als handgeschriebene Hooks und setzt nicht voraus, dass man die Abhängigkeiten korrekt auflistet. React bezeichnet diese automatische Memorisierung für Komponenten und Hooks so.

Achten Sie jedoch auf den Kontext in dieser Beschreibung – jeder Eintrag in der Liste ist eine React-Konstruktion.

Ein mentales Modell: Ein Render-Optimierer

Es hilft, sich den Compiler als etwas vorzustellen, das über zwei Arten von React-Arbeiten liegt und beide strafft:

React Compiler
                      │
          ┌───────────┴───────────┐
          ▼                       ▼
    Component work          Hook calculations
          │                       │
          ▼                       ▼
     memoization             memoization
          │                       │
          └───────────┬───────────┘
                      ▼
              fewer unnecessary
                 React updates

Schlechte Backend-Systeme sind außerhalb seiner Reichweite

Betrachten Sie ein asynchrones Komponente, das auf eine API wartet, bevor es ein Dashboard anzeigt:

async function Dashboard() {
  const data = await fetch(
    "https://api.example.com/dashboard"
  );

Sobald die Daten eingetroffen sind, werden sie an die Benutzeroberfläche weitergeleitet:

  return <DashboardUI data={data} />;
}

Falls dieser Endpunkt 800 ms benötigt, wird der Compiler die Laufzeit nicht auf 50 ms reduzieren. Er kann zwar den Prozess, Daten in eine Benutzeroberfläche umzuwandeln, vereinfachen, aber nicht dafür sorgen, dass das Backend schneller reagiert. Als Zeitstrahl:

Browser
   │
   ▼
React
   │
   ▼
API request ──────────► 800ms
   │
   ▼
Response
   │
   ▼
React render

Die Memoisierung findet fast ausschließlich im letzten Schritt statt. Wenn die meiste Zeit mit Warten auf das Netzwerk verbracht wird, optimiert man die falsche Ebene; Lösungen sind schnellere Abfragen, Caching, kleinere Antworten oder paralleles Abrufen. Um die Netzwerkzeit von der Client-Zeit zu trennen, siehe Diagnose von React-Leistungsproblemen jenseits der API-Antwortzeit.

JavaScript, das man nicht veröffentlichen sollte

Dieselbe Beschränkung gilt für die Größe des Bundles. Stellen Sie sich eine Startseite vor, die bereits vor dem Öffnen der Seiten, die diese Komponenten benötigen, eine schwere Charting-Bibliothek, einen Rich-Text-Editor sowie ein Analytics-SDK lädt:

Initial page
     │
     ├── React
     ├── UI
     ├── Chart library
     ├── Editor
     └── Analytics SDK

Der Compiler entfernt keine Abhängigkeiten; jedes Byte wird weiterhin heruntergeladen, analysiert und ausgeführt. Hilfreich ist das Code-Splitting, beispielsweise das Nachladen der Analytics-Seite auf Anfrage:

const Analytics = lazy(
  () => import("./Analytics")
);

Nun wird der Code nur dann geladen, wenn die Funktion tatsächlich dargestellt wird, und ein umgebender Suspense-Bereich zeigt während des Ladevorgangs eine Ersatzlösung an. Dies ist eine Entscheidung im Rahmen der Bundle-Architektur, und keine Menge an Memoisierung kann sie ersetzen.

Wo die Erwartungen fehlschlagen

Eine häufige Situation: Eine Anwendung wirkt träge, jemand aktiviert den Compiler, und das Team erwartet folgendes Ergebnis:

Slow app
   │
   ▼
React Compiler
   │
   ▼
Fast app

Das ist zu einfach. Langsamkeit hat mehrere Ursachen, und der Compiler behebt nur einen Aspekt davon:

SLOW APP
                       │
       ┌───────────────┼───────────────┐
       ▼               ▼               ▼
    React work      Network          Browser
       │               │               │
       ▼               ▼               ▼
   Compiler        API/cache       JS/rendering
       │               │               │
       ▼               ▼               ▼
   Optimized       Engineer         Engineer

Der Compiler ist nur ein Werkzeug im Leistungs-Toolkit, nicht das Toolkit selbst. Netzwerkverzögerungen sowie Browserkosten wie die Auswertung von Skripten und das Layout erfordern weiterhin menschliches Eingreifen.

Sollten Sie vorhandene useMemo-Aufrufe löschen?

Nicht automatisch. In neuem Code ist die offizielle Standardpraxis, dem Compiler die Memoisierung zu überlassen. Für bestehenden Code empfiehlt React, die aktuelle Memoisierung beizubehalten oder sie erst nach sorgfältiger Prüfung zu entfernen. Der Grund liegt im Verhalten und nicht nur in der Leistung: Ein gememoisierter Wert, der als Abhängigkeit eines Effects verwendet wird, bestimmt, wann dieser Effect erneut ausgeführt wird; daher kann eine Änderung der Memoisierungsweise dazu führen, dass Nebeneffekte zu unterschiedlichen Zeiten ausgelöst werden.

Widerstehen Sie daher dem Drang, eine solche umfassende Reinigung des Codebases durchzuführen:

useMemo      → DELETE
useCallback  → DELETE
React.memo   → DELETE

Ein sichererer Ansatz ist schrittweise Vorgehen mit Messungen bei jedem Schritt:

Existing app
     │
     ▼
Enable compiler
     │
     ▼
Run tests
     │
     ▼
Profile
     │
     ▼
Remove unnecessary
manual memoization
carefully

Der Compiler dient dazu, künftig eine manuelle Memoisierung weniger notwendig zu machen – nicht darum, über Nacht jede bestehende Optimierung für falsch zu erklären.

Der Compiler als Qualitätskontrolle für Code

Ein Vorteil, dem weniger Aufmerksamkeit geschenkt wird: Der Compiler beinhaltet Validierungsphasen, die auf den Rules of React basieren, und seine Integration in Lint-Tools kann Muster wie diese melden:

set-state-in-render
set-state-in-effect
unsafe ref access

React weist darauf hin, dass diese Diagnosen Fehler aufdecken können, die sich bereits in einer Anwendung befinden. Da der Compiler Code überspringt, der seine Annahmen verletzt, zeigen die Warnungen auch an, welche Komponenten nicht optimiert wurden, wodurch es Teil des täglichen Feedback-Zyklus wird und nicht nur ein Leistungs-Plugin.

Zuerst profilieren, dann die Lösung wählen

Wenn eine Schnittstelle langsam erscheint, messen Sie zunächst, bevor Sie ein Tool verwenden, und lassen Sie die Messungen bestimmen, welcher Bereich angegangen werden muss:

SLOW UI
                 │
                 ▼
               PROFILE
                 │
        ┌────────┼────────┐
        ▼        ▼        ▼
      React    Network   Browser
       work      │        │
        │        │        │
        ▼        ▼        ▼
    Compiler   API/cache  JS/render
        │
        ▼
   Targeted fix

Passen Sie anschließend die Lösung an das an, was der Profiler zeigt:

  • Unnötige React-Neurenderungen: Hier kann der React Compiler helfen.
  • Eine langsame API: Verbessern Sie den Endpunkt oder die Strategie zur Datenerfassung.
  • Ein überdimensionierter Datensatz: Senden Sie weniger JavaScript, indem Sie Daten aufteilen und Abhängigkeiten reduzieren.
  • Kostenintensive DOM-Änderungen: Überdenken Sie die Art und Weise, wie Sie Inhalte darstellen – beispielsweise durch Virtualisierung langer Listen.
  • Ein aufwändiges Script von Drittanbietern: Entfernen, verschieben oder isolieren Sie es.
  • Kernpunkte

    Das alte Modell erforderte, dass man React mitteilt, was gememorisert werden soll; das neue Modell ermöglicht es Ihnen, korrekten React-Code zu schreiben und dem Compiler den Großteil der Darstellungsaufgaben zu überlassen. Dadurch gewinnt eine Fähigkeit an Bedeutung: das Wissen darüber, wohin die Zeit tatsächlich geht.

    • Der Compiler optimiert nur die Aufgaben, die er versteht – also die React-Darstellung und Berechnungen mit Hooks.
    • Er kann keine überdimensionierten API-Antworten verkleinern, Datenbankabfragen beschleunigen, Scripts von Drittanbietern entfernen oder 5 MB große Bilder komprimieren.
    • Er kann keine Architektur retten, die überall unnötige Aufwände verursacht.
    • In vorhandenem Code sollte es zunächst aktiviert, getestet und analysiert werden, bevor die manuelle Memoisierung entfernt wird.
    • Seine Diagnosen sollten als kostenlose Fehlerberichte über Verstöße gegen die Rules of React betrachtet werden.

    Fragen Sie nicht, ob der Compiler aktiv ist, sondern wo die App ihre Zeit verbringt; genau dafür dient der Compiler, den Anteil von React daran zu überwachen.