React reaktiv machen: Das Streben nach leistungsstarken, leicht zu wartenden React-Apps
Edit 2-3-2016: Mobservable wurde in MobX umbenannt.
Wie erstellt man blitzschnelle React-Apps? Vor Kurzem haben wir begonnen, React in einem unserer Großprojekte zu verwenden, und es hat uns dank seiner strukturierten Art, Komponenten zu erstellen, und seines schnellen virtuellen DOM, das Unmengen an UI-Updates erspart, eine große Hilfe gewesen. Das Schöne an diesem Projekt ist, dass es einige nette Herausforderungen mit sich bringt; es muss Tausende von Objekten im Browser zeichnen und diese Objekte sind stark miteinander gekoppelt. Werte eines Objekts können in einer beliebigen Anzahl anderer Objekte verwendet werden, sodass kleine Änderungen Aktualisierungen in vielen unabhängigen Teilen der UI erfordern können. Diese Werte können durch Drag-and-Drop-Aktionen des Benutzers aktualisiert werden, sodass alle Aktualisierungen und Neuzeichnungen in weniger als 40 Millisekunden erfolgen müssen, damit die UI reaktionsfähig bleibt. Und obwohl reines React schnell ist, haben wir schnell gemerkt, dass React allein die Aufgabe nicht erfüllen würde.
Also begannen wir mit der Suche nach einer Lösung, die uns die benötigte Leistung bietet und gleichzeitig die Wartung unserer Codebasis nach React-Prinzipien ermöglicht. Kurz gesagt, wir wollten eine elegant Lösung. Also haben wir versucht, ein Konzept aus der Welt der funktionalen reaktiven Programmierung zu nutzen, nämlich Beobachtbare. Das Verkaufsargument von Observablen besteht darin, dass alle Berechnungen automatisch erkennen, welche anderen Observablen sie verwenden. Die Berechnung wird dann automatisch neu ausgewertet, wenn sich eines dieser Observablen in der Zukunft ändert. Beobachtbare sind ein Konzept, das in anderen UI-Frameworks wie Ember und Knockout verwendet wird. Wir haben herausgefunden, dass, wenn alle unsere Modellobjekte Beobachtbare und alle unsere React-Komponenten wurden Beobachter des Modells müssten wir keine weitere Magie anwenden, um sicherzustellen, dass der relevante Teil und nur der relevante Teil unserer Benutzeroberfläche aktualisiert wird. Lesen Sie weiter, um all die großartigen Dinge zu sehen, die enthüllt werden. Gegen Ende gibt es sogar Zahlen!
Beginnen wir mit einem konstruierten Beispiel, um das Ganze weniger theoretisch (oder weniger kompliziert, wenn Sie das bevorzugen) zu machen. Stellen Sie sich eine React-App vor, die einen kleinen Laden darstellt. Es gibt einige Artikel und einen Einkaufswagen, in den Sie einige dieser Artikel legen können. Etwa so:

Puh, dort es ist im wirklichen Leben.
Das Datenmodell
Definieren wir zunächst das Datenmodell. Es gibt Artikel mit Namen und Preis und einen Einkaufswagen mit Gesamtkosten, die sich aus der Summe seiner Einträge ergeben. Jeder Eintrag bezieht sich auf einen Artikel, speichert einen Betrag und hat einen abgeleiteten Preis. Die Beziehungen innerhalb unseres Datenmodells werden unten visualisiert. Offene Aufzählungszeichen stellen die abgeleiteten Daten dar, die aktualisiert werden sollten, wenn sich andere Daten ändern, und dies gilt auch für ihre Darstellung in der Benutzeroberfläche. Selbst in diesem einfachen Modell fließen also viele Daten umher, und es sind viele Benutzeroberflächen-Updates erforderlich, wenn sich Dinge ändern.

Lassen Sie uns eine Liste der Anforderungen zusammenstellen:
- Ändert sich der Preis eines Artikels, sollten auch alle zugehörigen Warenkorbeinträge einer Preisanpassung unterzogen werden.
- .. und das gilt auch für die Gesamtkosten des Einkaufswagens.
- Ändert sich die Anzahl der Artikel im Warenkorb, sollten die Gesamtkosten aktualisiert werden.
- Wenn ein Artikel umbenannt wird, sollte seine Ansicht aktualisiert werden
- Wenn ein Artikel umbenannt wird, sollte die Ansicht der zugehörigen Warenkorbeinträge aktualisiert werden
- Wenn ein neuer Artikel in den Warenkorb gelegt wird...
- etc .. etc ..
Wahrscheinlich ist jetzt der Kern unserer UI-Probleme klar. Als Programmierer möchten Sie keinen Standardcode schreiben, um alle möglichen Aktualisierungen zu verarbeiten, aber Ihr Benutzer muss möglicherweise unangenehm lange warten, wenn Ihre Anwendung bei jeder Datenänderung immer neu gerendert wird.
Lösen wir dieses Problem also ein für alle Mal und schreiben unser Datenmodell auf:
Ok, das war doch nicht so schwer, oder? Die obigen Konstruktorfunktionen basieren stark auf dem MobX Bibliothek, die eine eigenständige Implementierung des Observable-Konzepts bereitstellt (sie sollte sich genauso einfach mit anderen JavaScript-basierten Bibliotheken kombinieren lassen wie mit React). Die props Funktion erstellt neue, beobachtbare Eigenschaften auf dem Zielobjekt, basierend auf den bereitgestellten Schlüsseln und den Typen der Werte. Aufgrund der beobachtbaren Natur aller Eigenschaften werden die obigen Funktionen automatisch (und nur) aktualisiert, wenn sich einige ihrer Abhängigkeiten ändern. Dies erfüllt sofort einige unserer Anforderungen, wie zum Beispiel die total Der Warenkorb wird automatisch aktualisiert, wenn neue Einträge hinzugefügt werden, sich die Preise von Artikeln ändern usw.
Die Benutzeroberfläche
Sehen heißt glauben, also bauen wir eine Benutzeroberfläche um dieses Modell herum. Wir erstellen einige React-Komponenten, die unsere Anfangsdaten rendern. Der folgende JSX-Ausschnitt zeigt eine Ansicht des Einkaufswagens, rendert alle Einträge im Einkaufswagen und zeigt den Gesamtpreis des Einkaufswagens an. Wie Sie sich vorstellen können, sind andere Komponenten in der App, wie die Ansicht der Artikel, sehr ähnlich.
Ziemlich unkompliziert, oder? Eine CartView-Komponente empfängt einen Einkaufswagen, rendert dessen Gesamtbetrag und seine einzelnen Artikel mithilfe der CartEntryView, die wiederum den Namen des zugehörigen Artikels und die gewünschte Anzahl der Artikel ausgibt. Gemäß den Best Practices von React sollte jeder aufgelistete Artikel eindeutig identifizierbar sein, daher weisen wir jedem Eintrag eine beliebige, aber unveränderliche ID zu. Die Schaltfläche „Entfernen“ verringert diesen Betrag um eins und wenn er Null erreicht, wird der gesamte Eintrag aus dem Einkaufswagen entfernt. Beachten Sie, dass nirgendwo in der removeArticle Funktion haben wir angegeben, dass die Benutzeroberfläche aktualisiert werden soll.
Der nächste große Schritt besteht darin, diese Komponenten dazu zu zwingen, mit dem Datenmodell auf dem neuesten Stand zu bleiben, beispielsweise wenn der Eintrag entfernt wird. Wie Sie dem Rendering-Code leicht entnehmen können, gibt es viele mögliche Datenübergänge; die Anzahl der Artikel kann sich ändern, die Gesamtkosten des Warenkorbs können sich ändern, der Name eines Artikels kann sich ändern, sogar der Verweis zwischen Eintrag und Artikel kann sich ändern. Wie werden wir auf all diese Änderungen reagieren?
Nun, das ist ziemlich unkompliziert. Verwenden Sie einfach mobxReact.observer von der mobx-react Paket zu jeder Komponente und das reicht aus, um alle unsere anderen Anforderungen zu erfüllen:
Warte, was, das ist alles? Ja, sieh dir einfach die Demo und den Quelltext des obigen an unter JSfiddle. Was ist also hier passiert? Die observer Die Funktion hat zwei Dinge für uns getan. Erstens hat sie die Renderfunktion der Komponente in eine beobachtbare Funktion umgewandelt. Zweitens wurde die Komponente selbst als Beobachter dieser Funktion registriert, sodass jedes Mal, wenn das Rendering veraltet, ein erneutes Rendering erzwungen wird. Diese Funktion (und der Dekorator bei Verwendung von ES6) stellt also sicher, dass bei jeder Änderung beobachtbarer Daten nur die relevanten Teile der Benutzeroberfläche aktualisiert werden. Probieren Sie einfach in der Beispiel-App herum und behalten Sie dabei das Protokollfeld im Auge und sehen Sie, wie die Benutzeroberfläche basierend auf Ihren tatsächlichen Aktionen und den tatsächlichen Daten aktualisiert wird:
- Versuchen Sie, einen Artikel umzubenennen, der sich nicht im Warenkorb befindet
- einen Artikel in den Warenkorb legen und ihn anschließend umbenennen
- einen Artikel in den Warenkorb legen und seinen Preis aktualisieren
- aus dem Warenkorb entfernen, den Preis erneut aktualisieren
- … usw. Sie werden feststellen, dass bei jeder Aktion die Mindestanzahl an Komponenten neu gerendert wird.
Da jede Komponente ihre eigenen Abhängigkeiten verfolgt, ist es normalerweise nicht erforderlich, die untergeordneten Elemente einer Komponente explizit neu zu rendern. Wenn beispielsweise der Gesamtbetrag des Einkaufswagens neu gerendert wird, müssen auch die Einträge nicht neu gerendert werden. Reacts eigene PureRenderMixin sorgt dafür, dass das nicht passiert.
Die Zahlen
Was haben wir also erreicht? Zum Vergleich: werden auf dieser Seite erläutert Sie können genau dieselbe App finden, jedoch ohne Observables und mit einem naiven Ansatz, bei dem alles neu gerendert wird. Bei nur wenigen Artikeln werden Sie keinen Unterschied feststellen, aber sobald die Anzahl der Artikel steigt, wird der Leistungsunterschied wirklich erheblich.


Das Erstellen großer Datenmengen und Komponenten verhält sich mit und ohne Observables sehr ähnlich. Sobald Daten jedoch geändert werden, beginnen die Observables wirklich zu glänzen. Das Aktualisieren von 10 Artikeln in einer Sammlung von 10,000 Elementen ist ungefähr zehnmal schneller! 2.5 Sekunden wurden auf 250 Millisekunden reduziert. Das ist der Unterschied zwischen einer verzögerten und einer nicht verzögerten Erfahrung. Woher kommt dieser Unterschied? Werfen wir zunächst einen Blick auf die React-Renderberichte nach dem Ausführen der Aktualisierungen im Szenario „10 Artikel in einer Liste mit 10000 Artikeln aktualisieren“ ohne Observables:

Wie man sehen kann, werden alle zwanzigtausend ArticleViews und CartEntryViews neu gerendert. Allerdings wurden laut React 2,145 der insgesamt 2,433 Millisekunden Renderzeit verschwendet. Verschwendet bedeutet: Zeit, die für die Ausführung von Renderfunktionen aufgewendet wurde, die nicht wirklich zu einer Aktualisierung des echten DOM geführt haben. Das deutet stark darauf hin, dass das naive Neurendern von allem eine große Verschwendung von CPU-Zeit ist, wenn es viele Komponenten gibt. Zum Vergleich hier der Bericht des gleichen Szenarios bei Verwendung von Observables:

Das ist ein großer Unterschied! Anstatt 20,006 Komponenten neu zu rendern, werden nur 31 Komponenten neu gerendert. Und was noch wichtiger ist: Es wird kein Abfall gemeldet! Das bedeutet, dass jede einzelne neu gerenderte Komponente tatsächlich etwas im DOM geändert hat. Und genau das wollten wir mit der Verwendung von Observables erreichen!
Aus dem Bericht geht hervor, dass der Großteil der verbleibenden Renderzeit, 243 der insgesamt 267 Millisekunden, für das Rendern der CartView aufgewendet wird, die nur erneut gerendert wird, um die Gesamtkosten des Einkaufswagens zu aktualisieren. Das erneute Rendern der CartView bedeutet jedoch auch, dass alle zehntausend Einträge erneut überprüft werden, um zu sehen, ob sich eines der Argumente für die CartEntryViews geändert hat. Indem wir also einfach die Gesamtsumme der CartView in ihre eigene Komponente, CartTotalView, einfügen, kann das gesamte Rendern der CartView übersprungen werden, wenn sich nur die Gesamtkosten ändern. Dadurch verringert sich unsere Renderzeit noch weiter auf etwa 60 Millisekunden (siehe die „optimierte“ Reihe im Diagramm oben). Das ist etwa 40-mal schneller als das gleiche Update in unserer Vanilla-React-App!
Fazit
Durch die Verwendung von Observables haben wir eine Anwendung erstellt, die um ein Vielfaches schneller ist als dieselbe Anwendung, die alle Komponenten einfach neu rendert. Und was (für Sie als Programmierer) ebenso wichtig ist: Wir haben dies erreicht, ohne die Wartbarkeit des Codes zu beeinträchtigen. Sehen Sie sich einfach den Quellcode der beiden oben verlinkten JSFiddles an. Die beiden Listings sind sehr ähnlich und beide sind gleichermaßen bequem zu handhaben.
Hätten wir dasselbe mit anderen Techniken erreichen können? Vielleicht. Es gibt zum Beispiel ImmutableJS, das das React-Rendering ebenfalls sehr schnell macht, indem es nur Komponenten aktualisiert, die geänderte Daten erhalten. Allerdings müssen Sie bei Ihrem Datenmodell viel größere Zugeständnisse machen. Schließlich sind veränderbare Klassen meiner Meinung nach am Ende etwas bequemer zu handhaben als ihre unveränderlichen Gegenstücke. Außerdem helfen Ihnen die unveränderlichen Datenstrukturen nicht dabei, Ihre berechneten Werte auf dem neuesten Stand zu halten. Bei unveränderlichen Daten würde das Ändern des Namens eines Artikels die ArticleView also sehr schnell neu rendern, aber dennoch keine vorhandenen CartEntryViews ungültig machen, die auf denselben Artikel verweisen.
Eine weitere Technik, die man zur Optimierung einer React-App anwenden kann, besteht darin, Ereignisse für jede mögliche Mutation in den Daten zu erstellen und Listener für diese Ereignisse zum richtigen Zeitpunkt und in den richtigen Komponenten zu (de-)registrieren. Dies führt jedoch zu Unmengen an Boilerplate-Code, dessen Wartung fehleranfällig ist. Außerdem glaube ich, dass ich einfach zu faul bin, solche Dinge zu tun.
Übrigens empfehle ich dringend, Controller oder Aktionsdispatcher als Abstraktion für die Aktualisierung Ihrer Modelldaten zu verwenden, um die Trennung der Belange in Ihrem Projekt klar zu wahren.
Zusammenfassend lässt sich sagen, dass die Kombination von React mit Observables in großen Projekten so gut funktioniert hat, dass ich manchmal Datenmutationen gesehen habe, die die Benutzeroberfläche in Sonderfällen, an die ich noch nicht einmal gedacht hatte, korrekt aktualisiert haben, ohne dass es zu Leistungsproblemen kam. Ich überlasse also die harte Arbeit, herauszufinden, wann und wie die Benutzeroberfläche so schnell wie möglich aktualisiert werden kann, React und Observables und konzentriere mich auf die interessanten Teile des Programmierens :).
Besprechen Sie diesen Beitrag auf Hacker News.