Eine der wichtigsten Entscheidungen, die Webentwickler treffen müssen, ist, wo sie Logik und Rendering in ihrer Anwendung implementieren. Das kann schwierig sein, da es so viele Möglichkeiten gibt, eine Website zu erstellen.
Unser Verständnis dieses Bereichs basiert auf unserer Arbeit in Chrome mit großen Websites in den letzten Jahren. Im Allgemeinen empfehlen wir Entwicklern, serverseitiges oder statisches Rendering anstelle einer vollständigen Rehydrierung in Betracht zu ziehen.
Um die Architekturen besser zu verstehen, aus denen wir bei dieser Entscheidung auswählen, benötigen wir eine einheitliche Terminologie und ein gemeinsames Framework für jeden Ansatz. Anschließend können Sie die Kompromisse der einzelnen Rendering-Ansätze aus der Perspektive der Seitenleistung besser bewerten.
Terminologie
Zuerst definieren wir einige Begriffe, die wir verwenden werden.
Rendering
- Serverseitiges Rendering (SSR)
- Eine App auf dem Server rendern, um HTML anstelle von JavaScript an den Client zu senden.
- Clientseitiges Rendering (CSR)
- Rendern einer App in einem Browser mithilfe von JavaScript zum Ändern des DOM
- Pre-Rendering
- Eine clientseitige Anwendung zur Build-Zeit ausführen, um ihren ursprünglichen Zustand als statisches HTML zu erfassen.
- Durstlöscher
- Clientseitige Skripts ausführen, um serverseitig gerendertem HTML Anwendungsstatus und Interaktivität hinzuzufügen. Bei der Hydrierung wird davon ausgegangen, dass sich das DOM nicht ändert.
- Rehydration
- Die Begriffe „Hydrierung“ und „Rehydrierung“ werden oft synonym verwendet. „Rehydrierung“ impliziert jedoch, dass das DOM regelmäßig mit dem neuesten Status aktualisiert wird, auch nach der ersten Hydrierung.
Leistung
- Time to First Byte (TTFB)
- Die Zeit zwischen dem Klicken auf einen Link und dem Laden des ersten Byte des Inhalts auf der neuen Seite.
- First Contentful Paint (FCP)
- Der Zeitpunkt, zu dem angeforderte Inhalte (Artikeltext usw.) sichtbar werden.
- Interaction to Next Paint (INP)
- Ein repräsentativer Messwert, der angibt, ob eine Seite schnell auf Nutzereingaben reagiert.
- Total Blocking Time (TBT)
- Ein Proxy-Messwert für INP, der berechnet, wie lange der Haupt-Thread während des Seitenaufbaus blockiert wurde.
Serverseitiges Rendering
Beim serverseitigen Rendering wird das vollständige HTML für eine Seite auf dem Server als Reaktion auf die Navigation generiert. So werden zusätzliche Roundtrips für das Abrufen von Daten und das Erstellen von Vorlagen auf dem Client vermieden, da der Renderer diese Vorgänge ausführt, bevor der Browser eine Antwort erhält.
Serverseitiges Rendering führt in der Regel zu einem schnellen FCP. Wenn Sie die Seitenlogik auf dem Server ausführen und dort rendern, müssen Sie nicht so viel JavaScript an den Client senden. So lässt sich die TTBT einer Seite reduzieren, was auch zu einem niedrigeren INP führen kann, da der Hauptthread beim Laden der Seite nicht so oft blockiert wird. Wenn der Haupt-Thread seltener blockiert wird, können Nutzerinteraktionen schneller ausgeführt werden.
Das ist sinnvoll, da beim serverseitigen Rendern nur Text und Links an den Browser des Nutzers gesendet werden. Diese Methode kann bei einer Vielzahl von Geräte- und Netzwerkbedingungen gut funktionieren und ermöglicht interessante Browseroptimierungen, z. B. das Streaming von Dokumenten.
Beim serverseitigen Rendern müssen Nutzer seltener warten, bis CPU-gebundenes JavaScript ausgeführt wird, bevor sie Ihre Website verwenden können. Auch wenn Sie JavaScript von Drittanbietern nicht vermeiden können, können Sie durch serverseitiges Rendern Ihre eigenen JavaScript-Kosten senken und so mehr Budget für den Rest gewinnen. Dieser Ansatz hat jedoch einen potenziellen Nachteil: Das Generieren von Seiten auf dem Server dauert Zeit, was die TTFB Ihrer Seite erhöhen kann.
Ob serverseitiges Rendern für Ihre Anwendung ausreicht, hängt weitgehend davon ab, welche Art von Anwendung Sie entwickeln. Es gibt eine lange Debatte über die korrekte Anwendung von serverseitigem und clientseitigem Rendering. Sie können jedoch immer serverseitiges Rendering für einige Seiten und nicht für andere verwenden. Einige Websites haben Hybrid-Rendering-Techniken erfolgreich eingeführt. Netflix rendert beispielsweise seine relativ statischen Landingpages serverseitig, während das JavaScript für interaktionsintensive Seiten prefetching wird. Dadurch haben diese clientseitig gerenderten Seiten eine bessere Chance, schnell geladen zu werden.
Mit vielen modernen Frameworks, Bibliotheken und Architekturen können Sie dieselbe Anwendung sowohl auf dem Client als auch auf dem Server rendern. Sie können diese Techniken für das serverseitige Rendern verwenden. Architekturen, bei denen das Rendern sowohl auf dem Server als auch auf dem Client erfolgt, sind jedoch eine eigene Klasse von Lösungen mit sehr unterschiedlichen Leistungsmerkmalen und Kompromissen. React-Nutzer können Server-DOM-APIs oder darauf basierende Lösungen wie Next.js für das serverseitige Rendering verwenden. Vue-Nutzer können die Anleitung zum serverseitigen Rendern von Vue oder Nuxt verwenden. Angular hat Universal.
Die meisten beliebten Lösungen verwenden jedoch eine Form der Hydrierung. Achten Sie daher auf die Ansätze, die Ihr Tool verwendet.
Statisches Rendering
Statisches Rendern erfolgt zur Build-Zeit. Dieser Ansatz bietet einen schnellen FCP sowie einen niedrigeren TBT und INP, sofern Sie die Menge an clientseitigem JavaScript auf Ihren Seiten begrenzen. Im Gegensatz zum serverseitigen Rendern wird auch ein gleichbleibend schneller TTFB erreicht, da das HTML für eine Seite nicht dynamisch auf dem Server generiert werden muss. Im Allgemeinen bedeutet statisches Rendern, dass für jede URL im Voraus eine separate HTML-Datei erstellt wird. Mit im Voraus generierten HTML-Antworten können Sie statische Renderings in mehreren CDNs bereitstellen, um Edge-Caching zu nutzen.
Es gibt viele verschiedene Lösungen für das statische Rendern. Tools wie Gatsby sind so konzipiert, dass Entwickler den Eindruck haben, ihre Anwendung werde dynamisch gerendert und nicht als Build-Schritt generiert. Tools zur Generierung statischer Websites wie 11ty, Jekyll und Metalsmith nutzen die statische Natur und bieten einen stärker vorlagenbasierten Ansatz.
Einer der Nachteile des statischen Renderings ist, dass für jede mögliche URL individuelle HTML-Dateien generiert werden müssen. Das kann schwierig oder sogar unmöglich sein, wenn Sie diese URLs im Voraus vorhersagen müssen und es sich um Websites mit einer großen Anzahl eindeutiger Seiten handelt.
React-Nutzer kennen vielleicht Gatsby, Next.js Static Export oder Navi. Mit diesen Tools lassen sich Seiten ganz einfach aus Komponenten erstellen. Statisches Rendern und Vorrendern verhalten sich jedoch unterschiedlich: Statisch gerenderte Seiten sind interaktiv, ohne dass viel clientseitiges JavaScript ausgeführt werden muss. Das Vorrendern verbessert hingegen den FCP einer Single-Page-Anwendung, die auf dem Client gestartet werden muss, damit Seiten wirklich interaktiv sind.
Wenn Sie sich nicht sicher sind, ob eine bestimmte Lösung statisches Rendern oder Prerendering verwendet, deaktivieren Sie JavaScript und laden Sie die Seite, die Sie testen möchten. Bei statisch gerenderten Seiten sind die meisten interaktiven Funktionen auch ohne JavaScript verfügbar. Auf vorgerenderten Seiten sind möglicherweise noch einige grundlegende Funktionen wie Links mit deaktiviertem JavaScript verfügbar, aber der Großteil der Seite ist inaktiv.
Ein weiterer nützlicher Test ist die Netzwerkdrosselung in den Chrome-Entwicklertools. Damit können Sie sehen, wie viel JavaScript heruntergeladen wird, bevor eine Seite interaktiv wird. Für das Prerendering ist in der Regel mehr JavaScript erforderlich, damit die Seite interaktiv wird. Dieses JavaScript ist in der Regel komplexer als der Ansatz der progressiven Verbesserung, der beim statischen Rendern verwendet wird.
Serverseitiges Rendering im Vergleich zum statischen Rendering
Serverseitiges Rendern ist nicht für alle Anwendungsfälle die beste Lösung, da die dynamische Natur erhebliche Rechenkosten verursachen kann. Viele serverseitige Rendering-Lösungen führen nicht zu einem schnellen Leeren des Puffers, verzögern TTFB oder verdoppeln die gesendeten Daten (z. B. Inline-Status, die von JavaScript auf dem Client verwendet werden). In React kann renderToString() langsam sein, da es synchron und Single-Threaded ist.
Neuere React-Server-DOM-APIs unterstützen Streaming. Dadurch kann der erste Teil einer HTML-Antwort schneller an den Browser gesendet werden, während der Rest noch auf dem Server generiert wird.
Um serverseitiges Rendering richtig zu implementieren, müssen Sie möglicherweise eine Lösung für Komponentencaching finden oder entwickeln, den Arbeitsspeicherverbrauch verwalten, Memoization-Techniken verwenden und andere Aspekte berücksichtigen. Sie verarbeiten oder erstellen dieselbe App oft zweimal, einmal auf dem Client und einmal auf dem Server. Serverseitiges Rendering, bei dem Inhalte schneller angezeigt werden, bedeutet nicht unbedingt weniger Arbeit für Sie. Wenn auf dem Client viel Arbeit anfällt, nachdem eine vom Server generierte HTML-Antwort auf dem Client eingegangen ist, kann dies trotzdem zu höheren TBT- und INP-Werten für Ihre Website führen.
Beim serverseitigen Rendern wird HTML-Code bei Bedarf für jede URL generiert. Das kann jedoch langsamer sein als das Bereitstellen statischer gerenderter Inhalte. Wenn Sie den zusätzlichen Aufwand nicht scheuen, können Sie die serverseitige Renderingzeit durch serverseitiges Rendering in Kombination mit HTML-Caching erheblich verkürzen. Der Vorteil des serverseitigen Renderings besteht darin, dass mehr „Live“-Daten abgerufen und auf eine vollständigere Reihe von Anfragen reagiert werden kann als beim statischen Rendering. Seiten, die personalisiert werden müssen, sind ein konkretes Beispiel für Anfragen, die sich nicht gut für das statische Rendern eignen.
Auch beim Erstellen einer PWA kann serverseitiges Rendering interessante Entscheidungen mit sich bringen. Ist es besser, das Caching von Service Workern für die gesamte Seite zu verwenden oder einzelne Inhalte serverseitig zu rendern?
Clientseitiges Rendering
Beim clientseitigen Rendering werden Seiten direkt im Browser mit JavaScript gerendert. Die gesamte Logik, das Abrufen von Daten, die Vorlagen und das Routing werden auf dem Client statt auf dem Server ausgeführt. Das Ergebnis ist, dass mehr Daten vom Server an das Gerät des Nutzers übertragen werden, was mit eigenen Nachteilen verbunden ist.
Clientseitiges Rendering kann für Mobilgeräte schwierig zu implementieren und schnell zu halten sein.
Mit etwas Aufwand, um ein knappes JavaScript-Budget einzuhalten und Wert in so wenigen Roundtrips wie möglich zu liefern, können Sie die Leistung von clientseitigem Rendering fast an die von rein serverseitigem Rendering anpassen. Sie können den Parser schneller zum Laufen bringen, indem Sie wichtige Skripts und Daten über <link rel=preload> bereitstellen. Wir empfehlen außerdem, Muster wie PRPL zu verwenden, damit sich erste und nachfolgende Navigationsvorgänge sofort anfühlen.
Der Hauptnachteil des clientseitigen Renderings besteht darin, dass die erforderliche Menge an JavaScript mit dem Wachstum einer Anwendung tendenziell zunimmt, was sich auf den INP einer Seite auswirken kann. Das wird besonders schwierig, wenn neue JavaScript-Bibliotheken, Polyfills und Drittanbietercode hinzukommen, die um Rechenleistung konkurrieren und oft verarbeitet werden müssen, bevor der Inhalt einer Seite gerendert werden kann.
Bei Anwendungen, die clientseitiges Rendering verwenden und auf große JavaScript-Bundles angewiesen sind, sollte aggressives Code-Splitting in Betracht gezogen werden, um TBT und INP beim Seitenaufbau zu senken. Außerdem sollte JavaScript per Lazy Loading geladen werden, damit nur das bereitgestellt wird, was der Nutzer benötigt und wenn es benötigt wird. Bei Anwendungen mit wenig oder keiner Interaktivität kann serverseitiges Rendering eine skalierbarere Lösung für diese Probleme darstellen.
Wenn Sie Single-Page-Anwendungen entwickeln, können Sie die Application Shell-Caching-Technik anwenden, indem Sie die wichtigsten Teile der Benutzeroberfläche ermitteln, die von den meisten Seiten gemeinsam genutzt werden. In Kombination mit Service Workern kann dies die wahrgenommene Leistung bei wiederholten Besuchen erheblich verbessern, da die Seite das HTML und die Abhängigkeiten der Anwendungsshell sehr schnell aus CacheStorage laden kann.
Bei der Rehydrierung wird serverseitiges und clientseitiges Rendering kombiniert.
Hydration ist ein Ansatz, mit dem die Nachteile von clientseitigem und serverseitigem Rendering durch die Kombination beider Methoden gemindert werden. Navigationsanfragen wie das vollständige Laden oder Neuladen einer Seite werden von einem Server verarbeitet, der die Anwendung in HTML rendert. Das für das Rendern verwendete JavaScript und die Daten werden dann in das resultierende Dokument eingebettet. Wenn dies sorgfältig erfolgt, wird ein schneller FCP wie beim serverseitigen Rendern erreicht. Anschließend wird das Rendern auf dem Client fortgesetzt.
Dies ist eine effektive Lösung, kann aber erhebliche Leistungseinbußen mit sich bringen.
Der Hauptnachteil des serverseitigen Renderings mit Rehydrierung besteht darin, dass es sich erheblich negativ auf TBT und INP auswirken kann, auch wenn es FCP verbessert. Serverseitig gerenderte Seiten können geladen und interaktiv erscheinen, können aber erst auf Eingaben reagieren, wenn die clientseitigen Skripts für Komponenten ausgeführt und Event-Handler angehängt wurden. Auf Mobilgeräten kann das mehrere Minuten dauern, was für den Nutzer verwirrend und frustrierend sein kann.
Ein Rehydrierungsproblem: Eine App zum Preis von zwei
Damit das clientseitige JavaScript genau dort weitermachen kann, wo der Server aufgehört hat, ohne alle Daten noch einmal anzufordern, mit denen der Server sein HTML gerendert hat, serialisieren die meisten Lösungen für das serverseitige Rendern die Antwort aus den Datenabhängigkeiten einer Benutzeroberfläche als Script-Tags im Dokument. Da dadurch viel HTML-Code dupliziert wird, kann die Rehydrierung mehr Probleme als nur eine verzögerte Interaktivität verursachen.
Der Server gibt als Reaktion auf eine Navigationsanfrage eine Beschreibung der Benutzeroberfläche der Anwendung zurück. Er gibt aber auch die Quelldaten zurück, die zum Erstellen dieser Benutzeroberfläche verwendet werden, sowie eine vollständige Kopie der Implementierung der Benutzeroberfläche, die dann auf dem Client gestartet wird. Die Benutzeroberfläche wird erst interaktiv, nachdem bundle.js geladen und ausgeführt wurde.
Leistungsmesswerte, die von echten Websites mit serverseitigem Rendering und Rehydrierung erfasst wurden, deuten darauf hin, dass dies selten die beste Option ist. Der wichtigste Grund ist die Auswirkung auf die Nutzerfreundlichkeit, wenn eine Seite zwar fertig aussieht, aber keine der interaktiven Funktionen funktioniert.
Es gibt Hoffnung für serverseitiges Rendering mit Rehydration. Kurzfristig kann die TTFB reduziert werden, wenn serverseitiges Rendering nur für Inhalte verwendet wird, die sich gut im Cache speichern lassen. Das Ergebnis ist dann ähnlich wie beim Prerendering. Die inkrementelle, progressive oder teilweise Rehydrierung könnte der Schlüssel sein, um diese Technik in Zukunft praktikabler zu machen.
Serverseitiges Rendering streamen und progressiv rehydrieren
Das serverseitige Rendering hat sich in den letzten Jahren weiterentwickelt.
Beim Streaming-Server-Side Rendering können Sie HTML in Chunks senden, die der Browser nach und nach rendern kann, sobald sie empfangen werden. So können Sie Ihren Nutzern Markup schneller zur Verfügung stellen und die FCP beschleunigen. In React sind Streams in renderToPipeableStream() asynchron, im Vergleich zu synchronen renderToString(). Das bedeutet, dass Backpressure gut gehandhabt wird.
Progressive Rehydration ist ebenfalls eine Überlegung wert (React hat sie implementiert). Bei diesem Ansatz werden einzelne Teile einer serverseitig gerenderten Anwendung nach und nach „hochgefahren“, anstatt die gesamte Anwendung auf einmal zu initialisieren, wie es derzeit üblich ist. So kann die Menge an JavaScript reduziert werden, die erforderlich ist, um Seiten interaktiv zu machen. Sie können das clientseitige Upgrade von Teilen der Seite mit niedriger Priorität aufschieben, um zu verhindern, dass der Haupt-Thread blockiert wird. Dadurch können Nutzerinteraktionen schneller erfolgen, nachdem der Nutzer sie initiiert hat.
Durch die progressive Rehydrierung lässt sich auch einer der häufigsten Fallstricke bei der Rehydrierung von serverseitigem Rendering vermeiden: Ein serverseitig gerenderter DOM-Baum wird zerstört und dann sofort neu aufgebaut. Das liegt meist daran, dass für das erste synchrone clientseitige Rendering Daten erforderlich waren, die noch nicht ganz bereit waren, oft ein Promise, das noch nicht aufgelöst wurde.
Teilweise Rehydrierung
Die teilweise Rehydrierung hat sich als schwierig zu implementieren erwiesen. Dieser Ansatz ist eine Erweiterung der progressiven Rehydrierung, bei der einzelne Teile der Seite (Komponenten, Ansichten oder Bäume) analysiert und die Teile mit geringer oder keiner Interaktivität identifiziert werden. Für jeden dieser meist statischen Teile wird der entsprechende JavaScript-Code in inerte Referenzen und dekorative Elemente umgewandelt, wodurch der clientseitige Speicherbedarf auf nahezu null reduziert wird.
Die teilweise Rehydrierung birgt jedoch eigene Probleme und Kompromisse. Das stellt einige interessante Herausforderungen für das Caching dar. Außerdem können wir bei der clientseitigen Navigation nicht davon ausgehen, dass serverseitig gerenderter HTML-Code für inerte Teile der Anwendung ohne vollständiges Laden der Seite verfügbar ist.
Trisomorphes Rendering
Wenn Service Worker für Sie infrage kommen, sollten Sie trisomorphes Rendern in Betracht ziehen. Mit dieser Technik können Sie das serverseitige Streaming-Rendering für anfängliche oder nicht JavaScript-basierte Navigationsvorgänge verwenden. Nachdem der Service Worker installiert wurde, kann er dann das Rendern von HTML für Navigationsvorgänge übernehmen. So können zwischengespeicherte Komponenten und Vorlagen auf dem neuesten Stand gehalten und SPA-ähnliche Navigationsvorgänge zum Rendern neuer Ansichten in derselben Sitzung ermöglicht werden. Dieser Ansatz funktioniert am besten, wenn Sie denselben Vorlagen- und Routingcode für den Server, die Clientseite und den Service Worker verwenden können.
SEO-Überlegungen
Bei der Auswahl einer Web-Rendering-Strategie berücksichtigen Teams häufig die Auswirkungen auf die Suchmaschinenoptimierung. Das serverseitige Rendern ist eine beliebte Methode, um eine „vollständig aussehende“ Darstellung zu liefern, die Crawler interpretieren können. Crawler können JavaScript verstehen, aber es gibt oft Einschränkungen bei der Darstellung. Clientseitiges Rendern kann funktionieren, erfordert aber oft zusätzliche Tests und Overhead. In letzter Zeit ist auch dynamisches Rendern eine Option, die in Betracht gezogen werden sollte, wenn Ihre Architektur stark von clientseitigem JavaScript abhängt.
Im Zweifelsfall können Sie mit dem Tool zum Test auf Optimierung für Mobilgeräte prüfen, ob Ihr gewählter Ansatz die gewünschte Wirkung erzielt. Sie sehen eine visuelle Vorschau davon, wie eine Seite für den Google-Crawler aussieht, den serialisierten HTML-Inhalt, der nach der Ausführung von JavaScript gefunden wird, und alle Fehler, die beim Rendern aufgetreten sind.
Fazit
Wenn Sie sich für einen Rendering-Ansatz entscheiden, sollten Sie messen und verstehen, wo Ihre Engpässe liegen. Überlegen Sie, ob Sie mit statischem oder serverseitigem Rendering einen Großteil der Anforderungen erfüllen können. Es ist in Ordnung, hauptsächlich HTML mit minimalem JavaScript zu verwenden, um eine interaktive Umgebung zu schaffen. Hier ist eine praktische Infografik, die das Server-Client-Spektrum zeigt:
Guthaben {:#credits}
Vielen Dank an alle für ihre Rezensionen und Inspirationen:
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson und Sebastian Markbåge.