Legendenzustand für fein abgestimmte Updates von React und React Native
Bringen Sie den „Hot State“ in die „Legend State“-Observablen, damit die Zeilen der Wiedergabeliste sowie die Timer nur die geänderten Zellen neu rendern und nicht ganze React Native-Unterbäume.
Quellen: Legend State: The Fastest React State Library You’re Probably Not Using
Die Optimierung von React Native-Listen ist ein bekanntes Thema im Bereich der Leistungssteigerung, und Bibliotheken wie Legend List bringen das Scrollverhalten nahe an die Werte von FlashList. Nach ausreichend vielen Produktionsaudits ist jedoch die Virtualisierung der Listen nicht mehr das dominierende Engpassproblem. Der oft übersehene Kostenfaktor ist vielmehr der State: wo er in dem Datenbaum gespeichert ist und wie viele Komponenten neu renderisiert werden, wenn sich ein einzelnes Feld ändert.
Die eingebauten Hooks (useState, useContext) sind leicht zugänglich, erweisen sich jedoch häufig als inkompatibel mit dem Prinzip „weniger rendern, kleiner rendern“. Viele externe Speichersysteme verbessern die Situation. Legend State, derzeit in einer 3.0 Beta-Version verfügbar, verwendet einen besonders fein abgestimmten Ansatz – indem er die Erstellung des States von der Abonnementslogik trennt – und wurde ausführlich auf Chain React 2026 vorgestellt.
Das Problem mit useState
Die Leistungsphilosophie hinter dieser Bibliothek ist unmissverständlich: Schnellere Apps verringern die Menge an Arbeit, die React und React Native leisten müssen – kleinere Datenstrukturen, weniger Renderungen. Die Standardfunktionen useState und useContext stehen diesem Ziel entgegen, denn das Eigentum an einem sich ändernden Wert zwingt den zugehörigen Komponenten (oder jeden Context-Verbraucher) dazu, sich zu aktualisieren.
Betrachten wir einen Chat-Bildschirm, der einen replyId im lokalen Zustand speichert:
function ChatScreen() {
const [replyId, setReplyId] = useState('')
return (
<View>
<ChatMessages replyId={replyId} />
<ChatComposer replyId={replyId} />
</View>
)
}
function ChatMessage({ id, replyId }) {
const isReply = id === replyId
// ...
}
Der ChatScreen selbst muss sich möglicherweise nicht neu rendern, wenn sich replyId ändert – nur die Kinderkomponenten, die diesen ID lesen. Wenn useState beim Elternteil verwendet wird, rendernt React dennoch ChatScreen erneut, weil sich der Wert des Hooks geändert hat. Memoisierung und der React-Compiler können diese Arbeit nicht überspringen: Die Zustandsänderung ist real, weshalb der zugehörige Komponenten sich neu rendernt und anschließend die Abgleichsprozesse auch dann die Kinderkomponenten durchlaufen, wenn die meisten Eigenschaften unverändert geblieben sind.
useContext wirkt manchmal wie ein Ausweg, führt aber meist eher zu einer breiten statt zu einer tiefen Struktur: Jeder Verbraucher eines Contexts wird neu gerendert, sobald sich ein Teil dieses Contexts ändert – selbst wenn der jeweilige Verbraucher das geänderte Feld niemals liest. Die Aufteilung in viele kleine Provider hilft, bis die Struktur dieses vertrauten Nestings annimmt:
function App() {
return (
<SomeProvider>
<SomeOtherProvider>
<YetAnotherProvider>
<ThisWillBeTheLastOneProvider>
<OkJustOneMoreProvider>
<FinalFinalProvider>
<SendHelpProvider>
<ActualApp />
</SendHelpProvider>
</FinalFinalProvider>
</OkJustOneMoreProvider>
</ThisWillBeTheLastOneProvider>
</YetAnotherProvider>
</SomeOtherProvider>
</SomeProvider>
)
}
Wie ernst ist dieses Problem wirklich?
Es ist ernst genug, um es zu messen. In einem Benchmark wurden drei Möglichkeiten zur Platzierung eines Timers in einer Musik-App verglichen, der viermal pro Sekunde abläuft: am Wurzelpunkt der App, in der Mitte der Struktur und nur im Leaf-Text-Komponenten:
// App root
function App() {
const [time, setTime] = useState(0)
return (
<Window>
<PlaybackArea time={time} />
<Playlist />
<BottomToolbar />
</Window>
)
}
// Playback area:
function PlaybackArea() {
const [time, setTime] = useState(0)
return (
<View>
<PlaybackTime time={time} />
<TheRestOfThePlaybackArea />
</View>
)
}
// Leaf text:
function ElapsedText() {
const [time, setTime] = useState(0)
return (
<Text>{time}</Text>
)
}
In diesem Test war der am Wurzelpunkt befindliche Timer etwa zehnmal langsamer als die Version, die in der Leaf-Komponente lag. Die Platzierung häufig aktualisierter Daten ist keine Nebensache bei Mikro-Optimierungen; sie spielt eine entscheidende Rolle, wenn die Updates häufig stattfinden.
Was macht Legend State anders?
Ein Beispiel für eine Playliste macht den Unterschied konkret: Der Hintergrund einer Spaltenzeile sollte sich ändern, wenn diese Spur zur aktiven wird. Eine useState-Version sieht oft so aus:
function MusicApp() {
const [playback, setPlayback] = useState(initialPlayback);
return <TrackList activeTrackId={playback.activeTrackId} />;
}
function TrackList({ activeTrackId }) {
return tracks.map((track) => (
<TrackRowWrapper
key={track.id}
track={track}
activeTrackId={activeTrackId}
/>
));
}
function TrackRowWrapper({ track, activeTrackId }) {
const isActive = activeTrackId === track.id;
return <TrackRowView track={track} isActive={isActive} />;
}
Teams versuchen, die Kosten zu verringern, indem sie in einem Wrapper ein boolesches Feld isActive berechnen und dieses weiterleiten, sodass eine gememorierte TrackRowView überspringen kann, wenn sich das Flag nicht geändert hat. Das hilft zwar, doch jedes setPlayback lädt weiterhin MusicApp, TrackList sowie alle Wrapper in der Liste neu – obwohl eigentlich nur zwei Zeilenansichten (alte aktive und neue aktive) aktualisiert werden müssen.
Die Lösung von Legend State ist, dass dieser Art von Zustand nicht in React gehören sollte. Man erstellt ein observable (manchmal auch Signal genannt). Die Erstellung des Stores und das Abonnieren daran sind getrennte Schritte:
// creates the state
const playback$ = observable({
tracks: [],
activeTrackId: null,
});
function TrackRow({ track }) {
const isActive = useValue(
// subscribes *only* to playback$.activeTrackId
() => playback$.activeTrackId.get() === track.id,
);
return (
<View style={[styles.row, isActive && styles.activeRow]}>
<Text>{track.title}</Text>
</View>
);
}
Der angehängte $ ist lediglich eine Namenskonvention, die „beobachtbar“ bedeutet; er hat keine speziellen Eigenschaften während der Ausführung. Das playback$-Objekt bleibt referenziell stabil. Eingebettete Felder ändern sich über set() oder assign(), doch die Identität des Beobachtbaren ändert sich nicht.
// ...
const isActive = useValue(
() => playback$.activeTrackId.get() === track.id,
);
// ...
Wenn sich activeTrackId ändert, wird der Selektor erneut ausgeführt. Eine Neugenerierung wird nur dann geplant, wenn sich das boolesche Ergebnis ändert. Bei einer Playlist mit 500 Tracks können alle 500 Selektoren durch günstige Zeichenkettenvergleiche außerhalb von React neu bewertet werden; bei 498 Tracks bleibt das Ergebnis unverändert, sodass React nie benachrichtigt wird. Nur die vorherige und nächste aktive Zeile wird neu generiert.
Noch feinere Kontrolle
Auch das kann mehr Arbeit bedeuten, als ein Hot Path verlangt. Das Ändern der Hintergrundfarbe führt dennoch zu einer erneuten Darstellung des gesamten TrackRow. Legend State stellt reaktive Primitive bereit (Namen, die mit $ beginnen), die reaktive Props entgegennehmen:
function TrackRow({ track }) {
return (
<$View
$style={() => [
styles.row,
playback$.activeTrackId.get() === track.id && styles.activeRow,
]}
/>
);
}
TrackRow wird nur einmal eingebunden und führt seinen Code bei späteren Aktualisierungen nicht erneut aus. Wenn sich activeTrackId ändert, wird nur die $style-Funktion innerhalb eines kleinen Wrapper ausgeführt, den die Bibliothek einfügt. Der Komponente ist dann nicht mehr „eine Funktion, die bei Datenänderungen erneut ausgeführt wird“, sondern „eine einmal eingerichtete Struktur mit kleinen, sich selbst aktualisierenden reaktiven Bereichen“.
Die hochleistungsstarke Syntax mag ungewohnt erscheinen. Im Alltag wird das Muster aus observable-plus-useValue aus dem vorherigen Abschnitt verwendet, das bereits schnell ist; die $-Komponenten dienen als Ausweg für besonders belastete Bereiche.
Weiterführende Informationen
Die gleiche Architektur ermöglicht mehr als nur das Trimmen von Renderings:
useObserveEffectreagiert auf Zustandsänderungen statt auf Render-Zyklen, wodurch viele brüchige Abhängigkeitsarrays beseitigt werden, dieuseEffectfördert.- Die Aktualisierungen enthalten den neuen Wert, den vorherigen Wert sowie den Pfad, der sich geändert hat – genügend Details, um über dem Store eine Funktion zum Zurücksetzen/Ausführen erneut zu ermöglichen.
- Diese detaillierten Änderungsprotokolle liefern außerdem die Grundlage für einen offline-first-Synchronisierungsmechanismus mit Backend-Plugins, die in der Bibliothek mit relativ wenig Konfigurationscode enthalten sind (ein Thema, das für sich allein ausreicht).
Für Teams, die bereits nach einer Lösung jenseits von useState suchen, lohnt sich ein genauer Blick auf Legend State, wenn fein abgestimmte Abonnements und optionale reaktive Primitiven wichtiger sind als die Anpassung an das beliebteste API-Format. Beginnen Sie mit Observables und selektiven Hooks; wenden Sie Komponenten mit $-Präfix erst dann an, wenn Profilanalysen zeigen, dass eine Zeile oder Zelle immer noch zu oft gerendert wird. Dieser Ansatz hält das mentale Modell zugänglich, während gleichzeitig Platz für die am häufigsten genutzten Abläufe in einer React Native-Oberfläche bleibt.