Server-Rendering in einer Umgebung ohne Server

Der Autor des Materials, dessen Übersetzung wir veröffentlichen, ist einer der Gründer des Webiny- Projekts - eines serverlosen CMS, das auf React, GraphQL und Node.js basiert. Er sagt, dass die Unterstützung einer Cloud-Plattform ohne Server mit mehreren Mandanten ein Unternehmen ist, das bestimmte Aufgaben hat. Es wurden bereits viele Artikel verfasst, in denen Standardtechnologien zur Optimierung von Webprojekten diskutiert werden. Dazu gehören Server-Rendering, die Verwendung fortschrittlicher Technologien zur Entwicklung von Webanwendungen, verschiedene Möglichkeiten zur Verbesserung der Anwendungserstellung und vieles mehr. Dieser Artikel ähnelt einerseits den anderen und unterscheidet sich andererseits von ihnen. Tatsache ist, dass es sich der Optimierung von Projekten widmet, die in einer Umgebung ohne Server ausgeführt werden.



Vorbereitung


Um Messungen durchzuführen, mit denen die Probleme des Projekts identifiziert werden können, verwenden wir webpagetest.org . Mit Hilfe dieser Ressource werden wir Anfragen erfüllen und Informationen über die Ausführungszeit verschiedener Operationen sammeln. Auf diese Weise können wir besser verstehen, was Benutzer bei der Arbeit mit dem Projekt sehen und fühlen.

Wir sind besonders an der Anzeige „Erste Ansicht“ interessiert, dh wie lange es dauert, eine Website von einem Benutzer zu laden, der ihn zum ersten Mal besucht. Dies ist ein sehr wichtiger Indikator. Tatsache ist, dass der Browser-Cache viele Engpässe bei Webprojekten verbergen kann.

Indikatoren, die die Merkmale der Standortbelastung widerspiegeln - Identifizierung von Problemen


Schauen Sie sich die folgende Tabelle an.


Analyse alter und neuer Indikatoren eines Webprojekts

Hier kann der wichtigste Indikator als „Zeit zum Starten des Renderns“ erkannt werden - die Zeit vor dem Start des Renderns. Wenn Sie sich diesen Indikator genau ansehen, sehen Sie, dass es in der alten Version des Projekts fast 2 Sekunden gedauert hat, um mit dem Rendern der Seite zu beginnen. Der Grund dafür liegt im Wesen der Single Page Application (SPA). Um die Seite einer solchen Anwendung auf dem Bildschirm anzuzeigen, müssen Sie zuerst das umfangreiche JS-Bundle laden (diese Phase des Seitenladens ist in der folgenden Abbildung als 1 markiert). Dann muss dieses Bundle im Haupt-Thread (2) verarbeitet werden. Und erst danach kann etwas im Browserfenster erscheinen.


(1) Laden Sie das JS-Bundle herunter. (2) Warten auf die Bündelverarbeitung im Hauptthread

Dies ist jedoch nur ein Teil des Bildes. Nachdem der Hauptthread das JS-Bundle verarbeitet hat, stellt er mehrere Anforderungen an die Gateway-API. In dieser Phase der Seitenverarbeitung sieht der Benutzer eine rotierende Ladeanzeige. Der Anblick ist nicht der angenehmste. Der Benutzer hat jedoch noch keinen Seiteninhalt gesehen. Hier ist ein Storyboard des Ladevorgangs der Seite.


Laden der Seite

All dies deutet darauf hin, dass der Benutzer, der eine solche Website besucht hat, bei der Arbeit mit ihm keine besonders angenehmen Empfindungen verspürt. Er ist nämlich gezwungen, 2 Sekunden lang auf eine leere Seite und dann noch eine Sekunde lang auf die Download-Anzeige zu schauen. Diese Sekunde wird zur Zeit der Seitenvorbereitung hinzugefügt, da nach dem Laden und Verarbeiten die JS-Bundle-API-Anforderungen ausgeführt werden. Diese Abfragen sind erforderlich, um die Daten zu laden und als Ergebnis die fertige Seite anzuzeigen.


Laden der Seite

Wenn das Projekt auf einem regulären VPS gehostet würde, wäre die zum Abschließen dieser API-Anforderungen erforderliche Zeit größtenteils vorhersehbar. Projekte, die in einer Umgebung ohne Server ausgeführt werden, sind jedoch vom berüchtigten Kaltstartproblem betroffen. Bei der Webiny-Cloud-Plattform ist die Situation noch schlimmer. AWS Lambda-Funktionen sind Teil von VPC (Virtual Private Cloud). Dies bedeutet, dass Sie für jede neue Instanz einer solchen Funktion ENI (Elastic Network Interface, Elastic Network Interface) initialisieren müssen. Dies erhöht die Kaltstartzeit von Funktionen erheblich.

Hier sind einige Zeitpläne zum Laden von AWS Lambda-Funktionen innerhalb von VPCs und außerhalb von VPCs.


AWS Lambda-Funktionslastanalyse innerhalb und außerhalb der VPC (Bild von hier )

Daraus können wir schließen, dass in dem Fall, in dem die Funktion innerhalb der VPC gestartet wird, die Kaltstartzeit um das Zehnfache erhöht wird.

Darüber hinaus muss hier ein weiterer Faktor berücksichtigt werden - Verzögerungen bei der Übertragung von Netzwerkdaten. Ihre Dauer ist bereits zum Zeitpunkt der Ausführung von API-Anforderungen enthalten. Anfragen werden vom Browser initiiert. Daher stellt sich heraus, dass zu dem Zeitpunkt, an dem die API auf diese Anforderungen antwortet, die Zeit hinzugefügt wird, die erforderlich ist, um die Anforderung vom Browser an die API zu senden, und die Zeit, die benötigt wird, um die Antwort von der API an den Browser zu senden. Diese Verzögerungen treten bei jeder Anforderung auf.

Optimierungsaufgaben


Basierend auf der obigen Analyse haben wir mehrere Aufgaben formuliert, die wir lösen mussten, um das Projekt zu optimieren. Hier sind sie:

  • Verbessern der Geschwindigkeit beim Ausführen von API-Anforderungen oder Reduzieren der Anzahl von API-Anforderungen, die das Rendern blockieren.
  • Reduzieren der Größe des JS-Bundles oder Konvertieren dieses Bundles in Ressourcen, die für die Ausgabe der Seite nicht erforderlich sind.
  • Hauptfaden entsperren.

Problemansätze


Hier sind einige Ansätze zur Lösung der Probleme, die wir in Betracht gezogen haben:

  1. Codeoptimierung im Hinblick auf eine schnellere Ausführung. Dieser Ansatz erfordert viel Aufwand und hohe Kosten. Die Vorteile, die sich aus einer solchen Optimierung ergeben, sind zweifelhaft.
  2. Erhöhen Sie die für AWS Lambda-Funktionen verfügbare RAM-Größe. Es ist einfach, die Kosten für eine solche Lösung liegen irgendwo zwischen mittel und hoch. Von der Anwendung dieser Lösung sind nur geringe positive Effekte zu erwarten.
  3. Die Verwendung eines anderen Weges, um das Problem zu lösen. Zwar wussten wir in diesem Moment noch nicht, was diese Methode war.

Am Ende haben wir den dritten Punkt auf dieser Liste ausgewählt. Wir haben folgendes argumentiert: „Was ist, wenn wir absolut keine API-Aufrufe benötigen? Was ist, wenn wir überhaupt auf das JS-Bundle verzichten können? Dies würde es uns ermöglichen, alle Probleme des Projekts zu lösen. “


Die erste Idee, die wir interessant fanden, war, einen HTML-Snapshot der gerenderten Seite zu erstellen und den Snapshot für Benutzer freizugeben.

Erfolgloser Versuch


Webiny Cloud ist eine AWS Lambda-basierte serverlose Infrastruktur, die Webiny-Sites unterstützt. Unser System kann Bots erkennen. Wenn sich herausstellt, dass die Anforderung vom Bot abgeschlossen wurde, wird diese Anforderung an die Puppeteer- Instanz umgeleitet, die die Seite mit Chrome ohne Benutzeroberfläche rendert. Der vorgefertigte HTML-Code der Seite wird an den Bot gesendet. Dies geschah hauptsächlich aus SEO-Gründen, da viele Bots nicht wissen, wie man JavaScript ausführt. Wir haben uns für den gleichen Ansatz für die Erstellung von Seiten entschieden, die für normale Benutzer bestimmt sind.


Dieser Ansatz funktioniert gut in Umgebungen, in denen JavaScript nicht unterstützt wird. Wenn Sie jedoch versuchen, einem Client, dessen Browser JS unterstützt, vorgerenderte Seiten zuzuweisen, wird die Seite angezeigt. Nach dem Herunterladen der JS-Dateien wissen die React-Komponenten jedoch einfach nicht, wo sie bereitgestellt werden sollen. Dies führt zu einer ganzen Reihe von Fehlermeldungen in der Konsole. Infolgedessen passte eine solche Entscheidung nicht zu uns.

Einführung in SSR


Die starke Seite von Server Side Rendering (SSR) ist, dass alle API-Anforderungen innerhalb des lokalen Netzwerks ausgeführt werden. Da sie von einem bestimmten System oder einer bestimmten Funktion verarbeitet werden, die in der VPC ausgeführt wird, sind Verzögerungen, die beim Ausführen von Anforderungen vom Browser an das Ressourcen-Backend auftreten, untypisch. Obwohl in diesem Szenario das Problem eines „Kaltstarts“ bestehen bleibt.

Ein zusätzlicher Vorteil der Verwendung von SSR besteht darin, dass wir dem Client eine solche HTML-Version der Seite zur Verfügung stellen, mit der die React-Komponenten nach dem Laden der JS-Dateien keine Probleme beim Mounten haben.

Und schließlich brauchen wir kein sehr großes JS-Bundle. Außerdem können wir auf API-Aufrufe verzichten, um die Seite anzuzeigen. Ein Bundle kann asynchron geladen werden, wodurch der Hauptthread nicht blockiert wird.

Im Allgemeinen können wir sagen, dass das Server-Rendering anscheinend die meisten unserer Probleme hätte lösen müssen.

So sieht die Site-Analyse nach dem Anwenden von serverseitigem Rendering aus.


Site-Metriken nach dem Anwenden des Server-Renderings

Jetzt werden API-Anforderungen nicht ausgeführt, und die Seite wird angezeigt, bevor das große JS-Bundle geladen wird. Wenn Sie sich jedoch die erste Anforderung genau ansehen, können Sie feststellen, dass es fast 2 Sekunden dauert, bis ein Dokument vom Server abgerufen wird. Reden wir darüber.

Problem mit TTFB


Hier diskutieren wir die TTFB-Metrik (Zeit bis zum ersten Byte, Zeit bis zum ersten Byte). Hier sind die Details der ersten Anfrage.


Erste Anfrage Details

Um diese erste Anforderung zu verarbeiten, müssen Sie folgende Schritte ausführen: Starten Sie den Node.js-Server, führen Sie das Server-Rendering durch, stellen Sie API-Anforderungen und führen Sie JS-Code aus, und geben Sie das Endergebnis an den Client zurück. Das Problem hierbei ist, dass dies alles im Durchschnitt 1-2 Sekunden dauert.

Unser Server, der das Server-Rendering durchführt, muss all diese Arbeiten ausführen und kann erst danach das erste Byte der Antwort an den Client übertragen. Dies führt dazu, dass der Browser sehr lange auf den Beginn der Antwort auf die Anfrage warten muss. Infolgedessen stellt sich heraus, dass Sie jetzt für die Ausgabe der Seite fast den gleichen Arbeitsaufwand wie zuvor produzieren müssen. Der einzige Unterschied besteht darin, dass diese Arbeit nicht auf der Clientseite, sondern auf dem Server beim Rendern des Servers ausgeführt wird.

Hier haben Sie möglicherweise eine Frage zum Wort "Server". Wir haben die ganze Zeit über das serverlose System gesprochen. Woher kommt dieser "Server"? Wir haben natürlich versucht, das Server-Rendering in AWS Lambda-Funktionen zu rendern. Es stellte sich jedoch heraus, dass dies ein sehr ressourcenintensiver Prozess ist (insbesondere musste der Speicher sehr stark erhöht werden, um mehr Prozessorressourcen zu erhalten). Darüber hinaus wird hier das bereits erwähnte Problem des „Kaltstarts“ hinzugefügt. Daher bestand die ideale Lösung darin, einen Node.js-Server zu verwenden, der die Site-Materialien lädt und sie serverseitig rendert.

Kehren wir zu den Konsequenzen der Verwendung von serverseitigem Rendering zurück. Schauen Sie sich das folgende Storyboard an. Es ist leicht zu erkennen, dass es sich nicht besonders von dem unterscheidet, das bei der Untersuchung des Projekts erhalten wurde, das beim Kunden durchgeführt wurde.


Laden von Seiten bei Verwendung von serverseitigem Rendering

Der Benutzer muss 2,5 Sekunden lang auf eine leere Seite schauen. Es ist traurig.

Wenn man sich diese Ergebnisse ansieht, könnte man denken, dass wir absolut nichts erreicht haben, das ist eigentlich nicht so. Wir hatten einen HTML-Schnappschuss der Seite, der alles enthielt, was wir brauchten. Diese Aufnahme war bereit, mit React zu arbeiten. Gleichzeitig war es während der Verarbeitung der Seite auf dem Client nicht erforderlich, API-Anforderungen auszuführen. Alle notwendigen Daten wurden bereits in HTML eingebettet.

Das einzige Problem war, dass das Erstellen dieses HTML-Snapshots zu lange dauerte. Zu diesem Zeitpunkt könnten wir entweder mehr Zeit in die Optimierung des Server-Renderings investieren oder einfach die Ergebnisse zwischenspeichern und den Clients einen Schnappschuss der Seite aus einem Redis-Cache geben. Wir haben genau das getan.

Caching Server Rendering Ergebnisse


Nachdem ein Benutzer die Webiny-Website besucht hat, überprüfen wir zunächst den zentralen Redis-Cache, um festzustellen, ob ein HTML-Snapshot der Seite vorhanden ist. In diesem Fall geben wir dem Benutzer eine Seite aus dem Cache. Im Durchschnitt senkte dies den TTFB auf 200-400 ms. Nach der Einführung des Caches stellten wir signifikante Verbesserungen der Projektleistung fest.


Laden von Seiten bei Verwendung von serverseitigem Rendering und Cache

Selbst der Benutzer, der die Site zum ersten Mal besucht, sieht den Inhalt der Seite in weniger als einer Sekunde.

Schauen wir uns an, wie das Wasserfalldiagramm jetzt aussieht.


Site-Metriken nach dem Anwenden von serverseitigem Rendering und Caching

Die rote Linie zeigt einen Zeitstempel von 800 ms an. Hier wird der Inhalt der Seite vollständig geladen. Außerdem können Sie hier sehen, dass die JS-Bundles nach ca. 1,3 s geladen sind. Dies hat jedoch keinen Einfluss auf die Zeit, die der Benutzer benötigt, um die Seite zu sehen. Gleichzeitig müssen Sie keine API-Aufrufe durchführen und den Hauptthread laden, um die Seite anzuzeigen.

Beachten Sie, dass temporäre Indikatoren zum Laden des JS-Bundles, Ausführen von API-Anforderungen und Ausführen von Vorgängen im Hauptthread weiterhin eine wichtige Rolle bei der Vorbereitung der Seite für die Arbeit spielen. Diese Investition von Zeit und Ressourcen ist erforderlich, damit die Seite „interaktiv“ wird. Dies spielt jedoch erstens keine Rolle für Suchmaschinen-Bots und zweitens für das Gefühl des „schnellen Ladens von Seiten“ bei Benutzern.

Angenommen, eine Seite ist "dynamisch". Beispielsweise wird in der Kopfzeile ein Link angezeigt, über den auf das Benutzerkonto zugegriffen werden kann, falls der Benutzer, der die Seite anzeigt, angemeldet ist. Nach dem serverseitigen Rendern wird die Allzweck-Seite an den Browser gesendet. Das heißt - eine, die Benutzern angezeigt wird, die nicht angemeldet sind. Der Titel dieser Seite ändert sich und spiegelt die Tatsache wider, dass sich der Benutzer erst angemeldet hat, nachdem das JS-Bundle geladen und die API-Aufrufe ausgeführt wurden. Hier handelt es sich um den TTI- Indikator (Time To Interactive, Zeit bis zur ersten Interaktivität).

Einige Wochen später stellten wir fest, dass unser Proxyserver die Verbindung zum Client nicht dort schließt, wo sie benötigt wird, falls das Server-Rendering als Hintergrundprozess gestartet wurde. Die Korrektur von buchstäblich einer Codezeile führte dazu, dass der TTFB-Indikator auf das Niveau von 50-90 ms reduziert wurde. Infolgedessen wurde die Site nun nach etwa 600 ms im Browser angezeigt.

Wir hatten jedoch ein anderes Problem ...

Problem mit der Cache-Ungültigmachung


"In der Informatik gibt es nur zwei komplexe Dinge: Cache-Ungültigmachung und Benennung von Entitäten."
Phil Carleton

Die Ungültigmachung des Caches ist in der Tat eine sehr schwierige Aufgabe. Wie kann man es lösen? Erstens können Sie den Cache häufig aktualisieren, indem Sie eine sehr kurze Speicherzeit für zwischengespeicherte Objekte festlegen (TTL, Time To Live, Lebensdauer). Dies führt manchmal dazu, dass Seiten langsamer als gewöhnlich geladen werden. Zweitens können Sie einen Cache-Ungültigmachungsmechanismus basierend auf bestimmten Ereignissen erstellen.

In unserem Fall wurde dieses Problem mit einer sehr kleinen TTL von 30 Sekunden gelöst. Wir haben aber auch die Möglichkeit erkannt, Clients veraltete Daten aus dem Cache bereitzustellen. Zu einem Zeitpunkt, an dem Clients solche Daten empfangen, wird der Cache im Hintergrund aktualisiert. Dank dessen konnten wir Probleme wie Verzögerungen und "Kaltstart" beseitigen, die für AWS Lambda-Funktionen typisch sind.

So funktioniert es Ein Benutzer besucht die Webiny-Website. Wir überprüfen den HTML-Cache. Wenn es einen Screenshot der Seite gibt, geben wir ihn dem Benutzer. Das Alter eines Bildes kann sogar einige Tage betragen. Indem wir diesen alten Snapshot in wenigen hundert Millisekunden an den Benutzer übergeben, starten wir gleichzeitig die Aufgabe, einen neuen Snapshot zu erstellen und den Cache zu aktualisieren. Normalerweise dauert es einige Sekunden, um diese Aufgabe abzuschließen, da wir einen Mechanismus erstellt haben, dank dessen wir immer eine bestimmte Anzahl von AWS Lambda-Funktionen haben, die bereits ausgeführt werden und betriebsbereit sind. Daher müssen wir während der Erstellung neuer Bilder keine Zeit für den Kaltstart von Funktionen aufwenden.

Infolgedessen geben wir immer Seiten aus dem Cache an Clients zurück. Wenn das Alter der zwischengespeicherten Daten 30 Sekunden erreicht, wird der Inhalt des Caches aktualisiert.

Caching ist definitiv ein Bereich, in dem wir noch etwas verbessern können. Beispielsweise erwägen wir die Möglichkeit, den Cache automatisch zu aktualisieren, wenn der Benutzer eine Seite veröffentlicht. Ein solcher Cache-Aktualisierungsmechanismus ist jedoch auch nicht ideal.

Angenommen, auf der Homepage einer Ressource werden die drei neuesten Blog-Beiträge angezeigt. Wenn der Cache beim Veröffentlichen einer neuen Seite aktualisiert wird, wird aus technischer Sicht nach der Veröffentlichung nur der Cache für diese neue Seite generiert. Der Cache für die Homepage ist veraltet.

Wir suchen immer noch nach Möglichkeiten, das Caching-System unseres Projekts zu verbessern. Bisher lag der Schwerpunkt jedoch auf der Behebung bestehender Leistungsprobleme. Wir glauben, dass wir bei der Lösung dieser Probleme gute Arbeit geleistet haben.

Zusammenfassung


Zuerst haben wir clientseitiges Rendering verwendet. Dann konnte der Benutzer die Seite im Durchschnitt in 3,3 Sekunden sehen. Jetzt ist diese Zahl auf ungefähr 600 ms gefallen. Es ist auch wichtig, dass wir jetzt auf die Download-Anzeige verzichten.

Um dieses Ergebnis zu erzielen, durften wir hauptsächlich Server-Rendering verwenden. Ohne ein gutes Caching-System stellt sich jedoch heraus, dass die Berechnungen einfach vom Client auf den Server übertragen werden. Dies führt dazu, dass sich die Zeit, die der Benutzer benötigt, um die Seite zu sehen, nicht wesentlich ändert.

Die Verwendung von Server-Rendering hat eine andere positive Qualität, die zuvor nicht erwähnt wurde. Wir sprechen über die Tatsache, dass es einfacher ist, Seiten auf schwachen Mobilgeräten anzuzeigen. Die Geschwindigkeit, mit der eine Seite für die Anzeige auf solchen Geräten vorbereitet wird, hängt von den bescheidenen Fähigkeiten ihrer Prozessoren ab. Mit dem Server-Rendering können Sie einen Teil der Last von ihnen entfernen. Es sollte beachtet werden, dass wir keine spezielle Studie zu diesem Thema durchgeführt haben, aber das System, über das wir verfügen, sollte dazu beitragen, die Anzeige der Website auf Telefonen und Tablets zu verbessern.

Im Allgemeinen können wir sagen, dass die Implementierung des Server-Renderings keine leichte Aufgabe ist. Und die Tatsache, dass wir eine Umgebung ohne Server verwenden, erschwert diese Aufgabe nur. Die Lösung unserer Probleme erforderte Codeänderungen und zusätzliche Infrastruktur. Wir mussten einen gut gestalteten Caching-Mechanismus erstellen. Aber im Gegenzug haben wir viel Gutes bekommen. Das Wichtigste ist, dass die Seiten unserer Website jetzt viel schneller als zuvor geladen werden und sich auf die Arbeit vorbereiten. Wir glauben, dass es unseren Nutzern gefallen wird.

Liebe Leser! Verwenden Sie Caching- und Server-Rendering-Technologien, um Ihre Projekte zu optimieren?

Source: https://habr.com/ru/post/de459306/


All Articles