Das Rendering-Verhalten in React verstehen

Veröffentlicht: 2020-11-16

Zusammen mit Leben, Tod, Schicksal und Steuern ist das Rendering-Verhalten von React eine der größten Wahrheiten und Geheimnisse im Leben.

Tauchen wir ein!

Wie alle anderen habe ich meine Front-End-Entwicklungsreise mit jQuery begonnen. Reine JS-basierte DOM-Manipulation war damals ein Albtraum, also war es das, was jeder tat. Dann wurden JavaScript-basierte Frameworks langsam so prominent, dass ich sie nicht länger ignorieren konnte.

Das erste, das ich lernte, war Vue. Ich hatte eine unglaublich harte Zeit, weil Komponenten und Zustand und alles andere ein völlig neues mentales Modell waren, und es war eine Menge Schmerz, alles einzupassen. Aber schließlich tat ich es und klopfte mir auf die Schulter. Herzlichen Glückwunsch, Kumpel, sagte ich mir, du hast den steilen Aufstieg geschafft; Jetzt werden die restlichen Frameworks, falls Sie sie jemals lernen müssen, sehr einfach sein.

Als ich eines Tages anfing, React zu lernen, wurde mir klar, wie sehr ich mich geirrt hatte. Facebook hat die Dinge nicht einfacher gemacht, indem es Hooks eingeworfen und allen gesagt hat: „Hey, benutze das von jetzt an. Aber schreiben Sie keine Klassen um; Klassen sind in Ordnung. Eigentlich nicht so sehr, aber es ist okay. Aber Hooks sind alles, und sie sind die Zukunft.

Ich habs? Groß!".

Irgendwann überquerte ich auch diesen Berg. Aber dann wurde ich von etwas so Wichtigem und Schwierigem wie React selbst getroffen: Rendern von .

Überraschung!!!

Wenn Sie in React auf Rendering und seine Geheimnisse gestoßen sind, wissen Sie, wovon ich spreche. Und wenn nicht, haben Sie keine Ahnung, was auf Sie zukommt!

Aber bevor Sie Zeit mit irgendetwas verschwenden, ist es eine gute Angewohnheit zu fragen, was Sie davon haben würden (im Gegensatz zu mir, der ein überdrehter Idiot ist und gerne alles nur um der Sache willen lernt). Wenn Ihr Leben als React-Entwickler gut läuft, ohne sich Gedanken darüber zu machen, was dieses Rendering ist, warum sollte es sich darum kümmern? Gute Frage, also beantworten wir diese zuerst und sehen uns dann an, was Rendering eigentlich ist.

Warum ist es wichtig, das Rendering-Verhalten in React zu verstehen?

Wir alle beginnen mit dem Erlernen von React, indem wir (heute funktionale) Komponenten schreiben, die etwas namens JSX zurückgeben. Wir verstehen auch, dass dieses JSX irgendwie in tatsächliche HTML-DOM-Elemente umgewandelt wird, die auf der Seite erscheinen. Die Seiten werden aktualisiert, wenn der Status aktualisiert wird, die Routen ändern sich wie erwartet und alles ist in Ordnung. Aber diese Ansicht, wie React funktioniert, ist naiv und eine Quelle vieler Probleme.

Während es uns oft gelingt, komplette React-basierte Apps zu schreiben, finden wir manchmal bestimmte Teile unserer Anwendung (oder die gesamte Anwendung) bemerkenswert langsam. Und das Schlimmste. . . wir haben keine einzige Ahnung warum! Wir haben alles richtig gemacht, wir sehen keine Fehler oder Warnungen, wir haben alle bewährten Praktiken des Komponentendesigns, der Codierungsstandards usw. befolgt, und hinter den Kulissen gibt es keine Netzwerk-Langsamkeit oder teure Geschäftslogik-Berechnungen.

Manchmal ist es ein ganz anderes Problem: An der Leistung ist nichts auszusetzen, aber die App verhält sich seltsam. Beispielsweise drei API-Aufrufe an das Authentifizierungs-Backend, aber nur einen an alle anderen. Oder einige Seiten werden zweimal neu gezeichnet, wobei der sichtbare Übergang zwischen den beiden Renderings derselben Seite eine störende UX erzeugt.

Ach nein! Nicht noch einmal!!

Das Schlimmste ist, dass in solchen Fällen keine externe Hilfe verfügbar ist. Wenn Sie in Ihr bevorzugtes Entwicklerforum gehen und diese Frage stellen, werden sie antworten: „Kann ich nicht sagen, ohne einen Blick auf Ihre App zu werfen. Können Sie hier ein minimales Arbeitsbeispiel anhängen?“ Nun, Sie können aus rechtlichen Gründen natürlich nicht die gesamte App anhängen, während ein winziges funktionierendes Beispiel dieses Teils dieses Problem möglicherweise nicht enthält, da es nicht mit dem gesamten System so interagiert, wie es in der eigentlichen App der Fall ist.

Aufgeschmissen? Ja, wenn du mich fragst.

Also, wenn Sie solche Tage des Leids nicht sehen wollen, schlage ich vor, dass Sie Verständnis entwickeln – und Interesse, darauf muss ich bestehen; Widerstrebend erworbenes Verständnis wird Sie in der Welt von React nicht weit bringen – in diesem kaum verstandenen Ding namens Rendering in React. Vertrauen Sie mir, es ist nicht so schwer zu verstehen, und obwohl es sehr schwer zu meistern ist, werden Sie wirklich weit kommen, ohne jeden Winkel und jede Ritze kennen zu müssen.

Was bedeutet Rendern in React?

Das, mein Freund, ist eine ausgezeichnete Frage. Wir fragen es normalerweise nicht, wenn wir React lernen (ich weiß es, weil ich es nicht getan habe), weil uns das Wort „Rendering“ vielleicht in einem falschen Gefühl der Vertrautheit wiegt. Während die Bedeutung des Wörterbuchs völlig anders ist (und in dieser Diskussion nicht wichtig ist), haben wir Programmierer bereits eine Vorstellung davon, was es bedeuten sollte. Die Arbeit mit Bildschirmen, 3D-APIs, Grafikkarten und das Lesen von Produktspezifikationen trainiert unseren Verstand, an etwas wie „ein Bild malen“ zu denken, wenn wir das Wort „Rendern“ lesen. Bei der Game-Engine-Programmierung gibt es einen Renderer, dessen einzige Aufgabe es ist, – genau! – die Welt so zu malen, wie sie von der Szene übergeben wird.

Wir denken also, dass React, wenn es etwas „rendert“, alle Komponenten sammelt und das DOM der Webseite neu zeichnet. Aber in der React-Welt (und ja, sogar in der offiziellen Dokumentation) geht es beim Rendern nicht darum. Also, schnallen wir uns an und tauchen wirklich tief in die Interna von React ein.

"Ich werde verdammt sein . . .“

Sie müssen gehört haben, dass React ein sogenanntes virtuelles DOM verwaltet und es regelmäßig mit dem tatsächlichen DOM vergleicht und bei Bedarf Änderungen anwendet (aus diesem Grund können Sie jQuery und React nicht einfach zusammen einwerfen – React muss die volle Kontrolle darüber übernehmen der Dom). Nun, dieses virtuelle DOM besteht nicht wie das echte DOM aus HTML-Elementen, sondern aus React-Elementen. Was ist der Unterschied? Gute Frage! Warum nicht eine kleine React-App erstellen und selbst sehen?

Zu diesem Zweck habe ich diese sehr einfache React-App erstellt. Der gesamte Code ist nur eine einzige Datei mit ein paar Zeilen:

 import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }

Beachten Sie, was wir hier tun?

Ja, einfach protokollieren, wie ein JSX-Element aussieht. Diese JSX-Ausdrücke und -Komponenten haben wir hunderte Male geschrieben, aber wir achten selten darauf, was vor sich geht. Wenn Sie die Entwicklungskonsole Ihres Browsers öffnen und diese App ausführen, sehen Sie ein Object , das erweitert wird zu:

Das mag einschüchternd aussehen, aber beachte ein paar interessante Details:

  • Was wir betrachten, ist ein einfaches, reguläres JavaScript-Objekt und kein DOM-Knoten.
  • Beachten Sie, dass die Eigenschaft props besagt, dass es einen className von App hat (was die im Code festgelegte CSS-Klasse ist) und dass dieses Element zwei untergeordnete Elemente hat (dies stimmt auch überein, wobei die untergeordneten Elemente die Tags <h1> und <h2> sind). .
  • Die Eigenschaft _source sagt uns, wo der Quellcode den Körper des Elements beginnt. Wie Sie sehen können, nennt es die Datei App.js als Quelle und erwähnt Zeile Nummer 6. Wenn Sie sich den Code noch einmal ansehen, werden Sie feststellen, dass Zeile 6 direkt nach dem öffnenden JSX-Tag steht, was Sinn macht. Die JSX-Klammern enthalten das React-Element; sie sind nicht Teil davon, da sie dazu dienen, sich später in einen React.createElement() Aufruf umzuwandeln.
  • Die Eigenschaft __proto__ sagt uns, dass dieses Objekt alle seine ableitet. -Eigenschaften aus dem Root-JavaScript Object , was wiederum die Idee verstärkt, dass es sich hier nur um alltägliche JavaScript-Objekte handelt.

Jetzt verstehen wir also, dass das sogenannte virtuelle DOM überhaupt nicht wie das echte DOM aussieht, sondern ein Baum von React-Objekten (JavaScript) ist, die die Benutzeroberfläche zu diesem Zeitpunkt darstellen.

*SEUFZEN* . . . Sind wir schon da?

Erschöpft?

Glaub mir, ich bin es auch. Diese Ideen immer und immer wieder in meinem Kopf zu drehen, um zu versuchen, sie auf die bestmögliche Weise zu präsentieren, und dann über die Worte nachzudenken, um sie hervorzubringen und neu zu ordnen – ist nicht einfach.

Aber wir werden abgelenkt!

Nachdem wir so weit überlebt haben, sind wir jetzt in der Lage, die Frage zu beantworten, nach der wir gesucht haben: Was wird in React gerendert?

Nun, Rendering ist der React-Engine-Prozess, der durch das virtuelle DOM geht und den aktuellen Zustand, die Requisiten, die Struktur, die gewünschten Änderungen in der Benutzeroberfläche usw. sammelt. React aktualisiert jetzt das virtuelle DOM mithilfe einiger Berechnungen und vergleicht auch das neue Ergebnis mit dem tatsächlichen DOM auf der Seite. Dieses Rechnen und Vergleichen nennt das React-Team offiziell „Versöhnung“, und wenn Sie an ihren Ideen und relevanten Algorithmen interessiert sind, können Sie die offiziellen Dokumente einsehen.

Zeit sich zu engagieren!

Sobald der Rendering-Teil abgeschlossen ist, startet React eine Phase namens „commit“, während der es die notwendigen Änderungen auf das DOM anwendet. Diese Änderungen werden synchron angewendet (eine nach der anderen, obwohl bald ein neuer Modus erwartet wird, der gleichzeitig funktioniert), und das DOM wird aktualisiert. Wann und wie genau React diese Änderungen anwendet, ist nicht unsere Sorge, da es etwas ist, das völlig unter der Haube ist und sich wahrscheinlich ständig ändern wird, wenn das React-Team neue Dinge ausprobiert.

Rendering und Leistung in React-Apps

Wir haben inzwischen verstanden, dass Rendern bedeutet, Informationen zu sammeln, und es muss nicht jedes Mal zu visuellen DOM-Änderungen führen. Wir wissen auch, dass das, was wir als „Rendering“ betrachten, ein zweistufiger Prozess ist, der Rendering und Commit umfasst. Wir werden nun sehen, wie das Rendern (und noch wichtiger, das erneute Rendern) in React-Apps ausgelöst wird und wie die Unkenntnis der Details dazu führen kann, dass Apps schlecht funktionieren.

Erneutes Rendern aufgrund einer Änderung in der übergeordneten Komponente

Wenn sich eine übergeordnete Komponente in React ändert (z. B. weil sich ihr Status oder ihre Props geändert haben), geht React den gesamten Baum dieses übergeordnete Element hinunter und rendert alle Komponenten neu. Wenn Ihre Anwendung viele verschachtelte Komponenten und viele Interaktionen hat, erleiden Sie jedes Mal, wenn Sie die übergeordnete Komponente ändern, unwissentlich einen enormen Leistungseinbruch (vorausgesetzt, es ist nur die übergeordnete Komponente, die Sie ändern wollten).

Richtig, das Rendering wird React nicht dazu veranlassen, das tatsächliche DOM zu ändern, da es während der Abstimmung erkennt, dass sich für diese Komponenten nichts geändert hat. Aber es ist immer noch CPU-Zeit und Speicherverschwendung, und Sie werden überrascht sein, wie schnell sich das summiert.

Neudarstellung aufgrund von Kontextänderungen

Die Context-Funktion von React scheint das beliebteste State-Management-Tool aller zu sein (etwas, wofür es überhaupt nicht gebaut wurde). Es ist alles so praktisch – packen Sie einfach die oberste Komponente in den Kontextanbieter, und der Rest ist eine einfache Sache! Die meisten React-Apps werden auf diese Weise erstellt, aber wenn Sie diesen Artikel bisher gelesen haben, haben Sie wahrscheinlich erkannt, was falsch ist. Ja, jedes Mal, wenn das Kontextobjekt aktualisiert wird, löst es eine massive Neudarstellung aller Baumkomponenten aus.

Die meisten Apps haben kein Leistungsbewusstsein, sodass es niemand bemerkt, aber wie bereits erwähnt, können solche Versäumnisse bei Apps mit hohem Volumen und hoher Interaktion sehr kostspielig sein.

Verbesserung der React-Rendering-Leistung

Was können wir angesichts all dessen tun, um die Leistung unserer Apps zu verbessern? Es stellt sich heraus, dass wir ein paar Dinge tun können, aber beachten Sie, dass wir nur im Zusammenhang mit funktionalen Komponenten diskutieren werden. Klassenbasierte Komponenten werden vom React-Team dringend entmutigt und sind auf dem Weg nach draußen.

Verwenden Sie Redux oder ähnliche Bibliotheken für die Zustandsverwaltung

Diejenigen, die die schnelle und schmutzige Welt von Context lieben, neigen dazu, Redux zu hassen, aber dieses Ding ist aus guten Gründen sehr beliebt. Und einer dieser Gründe ist die Leistung – die connect() -Funktion in Redux ist magisch, da sie (fast immer) nur die erforderlichen Komponenten korrekt rendert. Ja, folgen Sie einfach der Standard-Redux-Architektur, und die Leistung ist kostenlos. Es ist überhaupt keine Übertreibung, dass Sie die meisten Leistungsprobleme (und andere) sofort vermeiden, wenn Sie die Redux-Architektur übernehmen.

Verwenden Sie memo() , um Komponenten „einzufrieren“.

Der Name „Memo“ kommt von Memoization, was ein ausgefallener Name für Caching ist. Und wenn Sie nicht oft auf Caching gestoßen sind, ist es in Ordnung; Hier ist eine verwässerte Beschreibung: Jedes Mal, wenn Sie ein Berechnungs-/Operationsergebnis benötigen, sehen Sie an der Stelle nach, an der Sie frühere Ergebnisse gepflegt haben. Wenn Sie es großartig finden, geben Sie dieses Ergebnis einfach zurück. Wenn nicht, fahren Sie fort und führen Sie diese Operation/Berechnung durch.

Bevor wir direkt in memo() , sehen wir uns zunächst an, wie unnötiges Rendering in React auftritt. Wir beginnen mit einem einfachen Szenario: Ein winziger Teil der App-Benutzeroberfläche, der dem Benutzer anzeigt, wie oft ihm der Service/das Produkt gefallen hat (wenn Sie Probleme haben, den Anwendungsfall zu akzeptieren, denken Sie daran, wie Sie auf Medium „klatschen“ können ” mehrmals, um zu zeigen, wie sehr Sie einen Artikel unterstützen/mögen).

Es gibt auch eine Schaltfläche, mit der sie die Likes um 1 erhöhen können. Und schließlich gibt es eine weitere Komponente, die den Benutzern ihre grundlegenden Kontodaten anzeigt. Machen Sie sich überhaupt keine Sorgen, wenn es Ihnen schwer fällt, dem zu folgen; Ich werde jetzt Schritt-für-Schritt-Code für alles bereitstellen (und es gibt nicht viel davon) und am Ende einen Link zu einem Spielplatz, wo Sie mit der funktionierenden App herumspielen und Ihr Verständnis verbessern können.

Lassen Sie uns zuerst die Komponente über Kundeninformationen angehen. Lassen Sie uns eine Datei namens CustomerInfo.js erstellen, die den folgenden Code enthält:

 import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };

Nichts Besonderes, oder?

Nur etwas Informationstext (der durch Requisiten hätte weitergegeben werden können), von dem nicht erwartet wird, dass er sich ändert, wenn der Benutzer mit der App interagiert (für die Puristen da draußen, ja, sicher, er kann sich ändern, aber der Punkt ist, im Vergleich zum Rest der Anwendung, es ist praktisch statisch). Aber beachten Sie die console.log() . Dies ist unser Hinweis darauf, dass die Komponente gerendert wurde (denken Sie daran, dass „gerendert“ bedeutet, dass ihre Informationen gesammelt und berechnet/verglichen wurden und nicht, dass sie auf das eigentliche DOM gemalt wurden).

Wenn wir also während unserer Tests keine solche Meldung in der Browserkonsole sehen, wurde unsere Komponente überhaupt nicht gerendert. Wenn es 10 Mal erscheint, bedeutet dies, dass die Komponente 10 Mal gerendert wurde. usw.

Und jetzt sehen wir uns an, wie unsere Hauptkomponente diese Kundeninfo-Komponente verwendet:

 import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); }

Wir sehen also, dass die App -Komponente einen internen Zustand hat, der durch den useState() Hook verwaltet wird. Dieser Status zählt weiter, wie oft der Benutzer den Dienst/die Seite gemocht hat, und wird anfänglich auf Null gesetzt. Keine Herausforderung für React-Apps, oder? Auf der UI-Seite sieht es so aus:

Der Knopf sieht zu verlockend aus, um nicht zertrümmert zu werden, zumindest für mich! Aber bevor ich das tue, öffne ich die Entwicklungskonsole meines Browsers und lösche sie. Danach werde ich den Knopf ein paar Mal zerschlagen, und hier ist, was ich sehe:

Ich habe 19 Mal auf die Schaltfläche gedrückt, und wie erwartet liegt die Gesamtzahl der Likes bei 19. Das Farbschema machte es wirklich schwer zu lesen, also fügte ich ein rotes Kästchen hinzu, um die Hauptsache hervorzuheben: die Komponente <CustomerInfo /> wurde 20 Mal gerendert!

Warum 20?

Einmal, als alles anfänglich gerendert wurde, und dann 19 Mal, als der Knopf gedrückt wurde. Die Schaltfläche ändert totalLikes , ein Zustandselement innerhalb der <App /> -Komponente, und infolgedessen wird die Hauptkomponente neu gerendert. Und wie wir in den früheren Abschnitten dieses Beitrags erfahren haben, werden alle darin enthaltenen Komponenten ebenfalls neu gerendert. Dies ist unerwünscht, da sich die <CustomerInfo /> -Komponente im Prozess nicht geändert hat und dennoch zum Rendering-Prozess beigetragen hat.

Wie können wir das verhindern?

Genau wie der Titel dieses Abschnitts sagt, verwenden Sie die memo() Funktion, um eine „konservierte“ oder zwischengespeicherte Kopie der <CustomerInfo /> -Komponente zu erstellen. Bei einer gespeicherten Komponente schaut sich React ihre Props an und vergleicht sie mit den vorherigen Props, und wenn es keine Änderung gibt, extrahiert React keine neue „Render“-Ausgabe aus dieser Komponente.

Lassen Sie uns diese Codezeile zu unserer CustomerInfo.js -Datei hinzufügen:

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

Ja, das ist alles, was wir tun müssen! Es ist jetzt an der Zeit, dies in unserer Hauptkomponente zu verwenden und zu sehen, ob sich etwas ändert:

 import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }

Ja, es wurden nur zwei Zeilen geändert, aber ich wollte trotzdem die gesamte Komponente zeigen. In Bezug auf die Benutzeroberfläche hat sich nichts geändert. Wenn ich also die neue Version für eine Runde drehe und ein paar Mal auf die Schaltfläche „Gefällt mir“ drücke, bekomme ich Folgendes:

Also, wie viele Konsolenmeldungen haben wir?

Einziger! Das bedeutet, dass die Komponente abgesehen vom anfänglichen Rendern überhaupt nicht berührt wurde. Stellen Sie sich die Leistungssteigerungen bei einer wirklich hochwertigen App vor! Okay, okay, der versprochene Link zum Code Playground ist hier. Um das vorherige Beispiel zu replizieren, müssen Sie CustomerInfo anstelle von MemoizedCustomerInfo aus CustomerInfo.js importieren und verwenden.

Allerdings ist memo() kein magischer Sand, den Sie überall verstreuen können und magische Ergebnisse erwarten. Auch die übermäßige Verwendung von memo memo() kann knifflige Fehler in Ihre App einführen und manchmal einfach dazu führen, dass einige erwartete Updates fehlschlagen. Auch hier gilt der allgemeine Hinweis zur „vorzeitigen“ Optimierung. Erstellen Sie zunächst Ihre App so, wie es Ihre Intuition sagt. Führen Sie dann ein intensives Profiling durch, um zu sehen, welche Teile langsam sind, und wenn sich herausstellt, dass gespeicherte Komponenten die richtige Lösung sind, führen Sie diese erst dann ein.

„Intelligentes“ Bauteildesign

Ich setze „intelligent“ in Anführungszeichen, weil: 1) Intelligenz höchst subjektiv und situativ ist; 2) Vermeintlich intelligente Handlungen haben oft unangenehme Folgen. Daher lautet mein Rat für diesen Abschnitt: Seien Sie nicht zu selbstsicher in dem, was Sie tun.

Nachdem dies aus dem Weg geräumt ist, besteht eine Möglichkeit zur Verbesserung der Renderleistung darin, Komponenten etwas anders zu entwerfen und zu platzieren. Beispielsweise kann eine untergeordnete Komponente umgestaltet und an eine Stelle in der Hierarchie nach oben verschoben werden, damit sie nicht erneut gerendert wird. Keine Regel besagt, dass „die ChatPhotoView-Komponente immer innerhalb der Chat-Komponente sein muss“. In besonderen Fällen (und das sind Fälle, in denen wir datengestützte Beweise dafür haben, dass die Leistung beeinträchtigt wird) kann das Biegen/Brechen der Regeln tatsächlich eine gute Idee sein.

Fazit

Es kann noch viel mehr getan werden, um React-Apps im Allgemeinen zu optimieren, aber da sich dieser Artikel auf das Rendern bezieht, habe ich den Umfang der Diskussion eingeschränkt. Unabhängig davon hoffe ich, dass Sie jetzt einen besseren Einblick in das haben, was in React unter der Haube vor sich geht, was Rendering eigentlich ist und wie es die Anwendungsleistung beeinflussen kann.

Lassen Sie uns als Nächstes verstehen, was React Hooks ist.