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