So identifizieren und beheben Sie verschwendete Renderings in React

Vor kurzem habe ich über das Leistungsprofil einer Reaktions-App nachgedacht, an der ich gearbeitet habe, und plötzlich ein paar Leistungsmetriken festgelegt. Und ich bin darauf gestoßen, dass das erste, was ich angehen muss, verschwendete Renderings sind, die ich auf jeder der Webseiten mache. Sie denken vielleicht darüber nach, was übrigens verschwendete Renderings sind? Lass uns eintauchen.

Von Anfang an hat React die gesamte Philosophie des Erstellens von Web-Apps und anschließend die Denkweise von Front-End-Entwicklern geändert. Mit der Einführung von Virtual DOM macht React UI-Updates so effizient wie nie zuvor. Dies macht die Web-App-Erfahrung ordentlich. Haben Sie sich jemals gefragt, wie Sie Ihre React-Anwendungen schneller machen können? Warum weisen mittelgroße React-Webanwendungen immer noch eine schlechte Leistung auf? Die Probleme liegen darin, wie wir React tatsächlich verwenden!

Wie React funktioniert

Eine moderne Front-End-Bibliothek wie React macht unsere App nicht auf wundersame Weise schneller. Zunächst sollten wir Entwickler verstehen, wie React funktioniert. Wie durchlaufen die Komponenten die Komponentenlebenszyklen während der Anwendungslebensdauer? Bevor wir uns mit einer Optimierungstechnik befassen, müssen wir besser verstehen, wie React tatsächlich unter der Haube funktioniert.

Im Kern von React verfügen wir über die JSX-Syntax und die leistungsstarke Fähigkeit von React, virtuelle DOMs zu erstellen und zu vergleichen. Seit seiner Veröffentlichung hat React viele andere Front-End-Bibliotheken beeinflusst. Zum Beispiel basiert Vue.js auch auf der Idee virtueller DOMs.

Jede React-Anwendung beginnt mit einer Root-Komponente. Wir können uns die gesamte Anwendung als eine Baumformation vorstellen, bei der jeder Knoten eine Komponente ist. Komponenten in React sind 'Funktionen', die die Benutzeroberfläche basierend auf den Daten rendern. Das bedeutet Requisiten und Zustand, den es erhält; sag das istCF

UI = CF(data)

Benutzer interagieren mit der Benutzeroberfläche und verursachen die Änderung der Daten. Die Interaktionen sind alles, was ein Benutzer in unserer Anwendung tun kann. Klicken Sie beispielsweise auf eine Schaltfläche, verschieben Sie Bilder, ziehen Sie Listenelemente herum, und AJAX fordert APIs auf. Alle diese Interaktionen ändern nur die Daten. Sie verursachen niemals Änderungen in der Benutzeroberfläche.

Daten sind hier alles, was den Status einer Anwendung definiert. Nicht nur das, was wir in unserer Datenbank gespeichert haben. Sogar verschiedene Front-End-Zustände, z. B. welche Registerkarte aktuell ausgewählt ist oder ob ein Kontrollkästchen derzeit aktiviert ist oder nicht, sind Teil dieser Daten. Bei jeder Datenänderung verwendet React die Komponentenfunktionen, um die Benutzeroberfläche neu zu rendern, jedoch nur virtuell:

UI1 = CF(data1)UI2 = CF(data2)

React berechnet die Unterschiede zwischen der aktuellen und der neuen Benutzeroberfläche, indem ein Vergleichsalgorithmus auf die beiden Versionen des virtuellen DOM angewendet wird.

Changes = Difference(UI1, UI2)

React wendet dann nur die Änderungen an der Benutzeroberfläche auf die reale Benutzeroberfläche im Browser an. Wenn sich die einer Komponente zugeordneten Daten ändern, bestimmt React, ob eine tatsächliche DOM-Aktualisierung erforderlich ist. Auf diese Weise kann React potenziell teure DOM-Manipulationsvorgänge im Browser vermeiden. Beispiele wie das Erstellen von DOM-Knoten und der Zugriff auf vorhandene Knoten über die Notwendigkeit hinaus.

Dieses wiederholte Unterscheiden und Rendern von Komponenten kann eine der Hauptursachen für React-Leistungsprobleme in jeder React-App sein. Erstellen einer React-App, bei der der Differenzierungsalgorithmus nicht effektiv abgeglichen werden kann, wodurch die gesamte App wiederholt gerendert wird, was tatsächlich zu verschwendeten Renderings führt und zu einer frustrierend langsamen Erfahrung führen kann.

Während des ersten Rendervorgangs erstellt React einen DOM-Baum wie folgt:

Angenommen, ein Teil der Daten ändert sich. Wir möchten, dass React nur die Komponenten neu rendert, die direkt von dieser bestimmten Änderung betroffen sind. Überspringen Sie möglicherweise sogar den Differenzierungsprozess für den Rest der Komponenten. Angenommen, einige Datenänderungen in der Komponente 2im obigen Bild, und diese Daten wurden von Ran Bund dann übergeben 2. Wenn R erneut rendert, wird jedes seiner untergeordneten Elemente erneut gerendert, was A, B, C, D bedeutet. Durch diesen Vorgang führt React tatsächlich Folgendes aus:

Im obigen Bild werden alle gelben Knoten gerendert und differenziert. Dies führt zu einer Verschwendung von Zeit / Rechenressourcen. Hier werden wir vor allem unsere Optimierungsbemühungen einsetzen. Konfigurieren Sie jede Komponente so, dass sie nur dann gerendert und unterschieden wird, wenn dies erforderlich ist. Auf diese Weise können wir diese verschwendeten CPU-Zyklen zurückgewinnen. Zunächst untersuchen wir, wie wir verschwendete Renderings unserer Anwendung identifizieren können.

Identifizieren Sie verschwendete Renderings

Es gibt verschiedene Möglichkeiten, dies zu tun. Die einfachste Methode besteht darin, die Option zum Hervorheben von Aktualisierungen in der Einstellung "React dev tools" zu aktivieren.

Während der Interaktion mit der App werden Aktualisierungen auf dem Bildschirm mit farbigen Rändern hervorgehoben. Bei diesem Vorgang sollten Komponenten angezeigt werden, die neu gerendert wurden. Auf diese Weise können wir nicht erforderliche Renderings erkennen.

Folgen wir diesem Beispiel.

Beachten Sie, dass bei der Eingabe einer zweiten Aufgabe bei jedem Tastendruck auch die erste Aufgabe auf dem Bildschirm blinkt. Dies bedeutet, dass es von React zusammen mit der Eingabe neu gerendert wird. Dies ist das, was wir als "verschwendetes" Rendering bezeichnen. Wir wissen, dass dies nicht erforderlich ist, da sich der Inhalt der ersten Aufgabe nicht geändert hat, aber React weiß dies nicht.

Obwohl React nur die geänderten DOM-Knoten aktualisiert, dauert das erneute Rendern noch einige Zeit. In vielen Fällen ist dies kein Problem, aber wenn die Verlangsamung spürbar ist, sollten wir einige Dinge berücksichtigen, um diese redundanten Renderings zu stoppen.

Verwenden der shouldComponentUpdate-Methode

Standardmäßig rendert React das virtuelle DOM und vergleicht die Differenz für jede Komponente im Baum auf Änderungen ihrer Requisiten oder ihres Status. Das ist aber offensichtlich nicht vernünftig. Wenn unsere App wächst, verlangsamt der Versuch, das gesamte virtuelle DOM bei jeder Aktion neu zu rendern und zu vergleichen, das Ganze.

React bietet eine einfache Lebenszyklusmethode, mit der angegeben wird, ob eine Komponente erneut gerendert werden muss, und shouldComponentUpdatedie vor dem Start des erneuten Renderns ausgelöst wird. Die Standardimplementierung dieser Funktion wird zurückgegeben true.

Wenn diese Funktion für eine Komponente true zurückgibt, kann der Render-Differenzierungsprozess ausgelöst werden. Dies gibt uns die Möglichkeit, den Render-Differenzierungsprozess zu steuern. Angenommen, wir müssen verhindern, dass eine Komponente erneut gerendert wird, wir müssen einfach falsevon dieser Funktion zurückkehren. Wie aus der Implementierung der Methode hervorgeht, können wir die aktuellen und nächsten Requisiten und den Status vergleichen, um festzustellen, ob ein erneutes Rendern erforderlich ist:

Mit reinen Komponenten

Während Sie an React arbeiten, wissen Sie definitiv, React.Componentaber worum geht es React.PureComponent? Wir haben bereits über die Lifecycle-Methode shouldComponentUpdate gesprochen. In reinen Komponenten gibt es bereits eine Standardimplementierung shouldComponentUpdate()mit einem flachen Prop- und Statusvergleich . Eine reine Komponente ist also eine Komponente, die nur dann neu gerendert wird, wenn sie props/statesich von den vorherigen Requisiten und dem vorherigen Status unterscheidet .

Im flachen Vergleich werden primitive Datentypen wie String, Boolean, Number nach Wert und komplexe Datentypen wie Array, Objekt und Funktion nach Referenz verglichen

Was aber, wenn wir eine funktionale zustandslose Komponente haben, in der wir diese Vergleichsmethode implementieren müssen, bevor jedes erneute Rendern erfolgt? React hat eine Komponente höherer Ordnung React.memo. Es ist wie React.PureComponentaber für funktionale Komponenten anstelle von Klassen.

Standardmäßig funktioniert es genauso wie shouldComponentUpdate (), das das Requisitenobjekt nur flach vergleicht. Aber wenn wir die Kontrolle über diesen Vergleich haben wollen? Als zweites Argument können wir auch eine benutzerdefinierte Vergleichsfunktion bereitstellen.

Daten unveränderlich machen

Was wäre, wenn wir eine verwenden könnten, React.PureComponentaber dennoch effizient erkennen können, wann sich komplexe Requisiten oder Zustände wie ein Array, ein Objekt usw. automatisch geändert haben? Hier erleichtert die unveränderliche Datenstruktur das Leben.

Die Idee hinter der Verwendung unveränderlicher Datenstrukturen ist einfach. Wie bereits erwähnt, wird bei komplexen Datentypen der Vergleich über deren Referenz durchgeführt. Wenn sich ein Objekt mit komplexen Daten ändert, können wir anstelle der Änderungen an diesem Objekt eine Kopie dieses Objekts mit den Änderungen erstellen, wodurch eine neue Referenz erstellt wird.

ES6 verfügt über einen Objektverteilungsoperator, um dies zu ermöglichen.

Dasselbe können wir auch für Arrays tun:

Vermeiden Sie es, eine neue Referenz für dieselben alten Daten zu übergeben

Wir wissen, dass bei jeder propsÄnderung einer Komponente ein erneutes Rendern erfolgt. Aber manchmal hat sich das propsnicht geändert. Wir schreiben Code so, dass React glaubt, dass er sich geändert hat, und dass dies auch zu einem erneuten Rendern führt, diesmal jedoch zu einem verschwendeten Rendern. Grundsätzlich müssen wir also sicherstellen, dass wir eine andere Referenz als Requisiten für verschiedene Daten übergeben. Außerdem müssen wir vermeiden, eine neue Referenz für dieselben Daten zu übergeben. Nun werden wir uns einige Fälle ansehen, in denen wir dieses Problem verursachen. Schauen wir uns diesen Code an.

Hier ist der Inhalt für die BookInfoKomponente, in der zwei Komponenten gerendert werden, BookDescriptionund BookReview. Dies ist der richtige Code und funktioniert einwandfrei, es liegt jedoch ein Problem vor. BookDescriptionwird neu gerendert, wenn wir neue Überprüfungsdaten als Requisiten erhalten. Warum? Sobald die BookInfoKomponente neue Requisiten erhält, wird die renderFunktion aufgerufen, um ihren Elementbaum zu erstellen. Die Renderfunktion erstellt eine neue bookKonstante, dh, es wird eine neue Referenz erstellt. Erhalten BookDescriptionSie dies also bookals Nachrichtenreferenz, die das erneute Rendern von verursacht BookDescription. Wir können diesen Code also wie folgt umgestalten:

Jetzt ist die Referenz immer dieselbe this.bookund beim Rendern wird kein neues Objekt erstellt. Diese Re-Rendering-Philosophie gilt für alle propEvent-Handler, einschließlich:

Hier haben wir zwei verschiedene Methoden verwendet (Bindungsmethoden und Verwendung der Pfeilfunktion beim Rendern), um die Event-Handler-Methoden aufzurufen, aber beide erstellen jedes Mal eine neue Funktion, wenn die Komponente erneut gerendert wird. Um diese Probleme zu beheben, können wir die Methode in die constructorund unter Verwendung von Klasseneigenschaften binden, die noch experimentell und noch nicht standardisiert sind, aber so viele Entwickler verwenden diese Methode bereits, um Funktionen in produktionsfertigen Anwendungen an andere Komponenten zu übergeben:

Einpacken

Intern verwendet React verschiedene clevere Techniken, um die Anzahl der kostspieligen DOM-Vorgänge zu minimieren, die zum Aktualisieren der Benutzeroberfläche erforderlich sind. Bei vielen Anwendungen führt die Verwendung von React zu einer schnellen Benutzeroberfläche, ohne dass viel Arbeit erforderlich ist, um die Leistung speziell zu optimieren. Wenn wir jedoch die oben genannten Techniken befolgen können, um verschwendete Renderings zu beheben, erhalten wir bei großen Anwendungen auch eine sehr reibungslose Erfahrung in Bezug auf die Leistung.