Hier erfahren Sie, was der Browser-Preload-Scanner ist, wie er die Leistung verbessert und wie Sie ihn aus dem Weg gehen können.
Ein Aspekt, der bei der Optimierung der Seitengeschwindigkeit oft übersehen wird, ist das Wissen über die internen Abläufe von Browsern. Browser führen bestimmte Optimierungen durch, um die Leistung auf eine Weise zu verbessern, wie wir das als Entwickler nicht können – aber nur, solange diese Optimierungen nicht versehentlich verhindert werden.
Eine interne Browseroptimierung ist der Browser-Preload-Scanner. In diesem Beitrag erfahren Sie, wie der Preloader funktioniert und vor allem, wie Sie ihn nicht behindern.
Was ist ein Preloader-Scanner?
Jeder Browser hat einen primären HTML-Parser, der das Roh-Markup tokenisiert und in ein Objektmodell verarbeitet. Das geht so weiter, bis der Parser eine blockierende Ressource findet, z. B. ein Stylesheet, das mit einem <link>
-Element geladen wurde, oder ein Script, das mit einem <script>
-Element ohne async
- oder defer
-Attribut geladen wurde.
Bei CSS-Dateien wird das Rendering blockiert, um ein Flash of Unstyled Content (FOUC) zu verhindern. Dabei wird eine unformatierte Version einer Seite kurz angezeigt, bevor Stile darauf angewendet werden.
Der Browser blockiert auch das Parsen und Rendern der Seite, wenn er <script>
-Elemente ohne defer
- oder async
-Attribut findet.
Der Grund dafür ist, dass der Browser nicht sicher weiß, ob ein bestimmtes Script das DOM ändert, während der primäre HTML-Parser noch seine Arbeit erledigt. Aus diesem Grund wird JavaScript häufig am Ende des Dokuments geladen, damit die Auswirkungen blockierter Parsing- und Rendering-Vorgänge marginal werden.
Das sind gute Gründe dafür, dass der Browser sowohl das Parsen als auch das Rendern blockieren sollte. Es ist jedoch nicht wünschenswert, einen dieser wichtigen Schritte zu blockieren, da dies die Suche nach anderen wichtigen Ressourcen verzögern kann. Glücklicherweise versuchen Browser, diese Probleme mithilfe eines sekundären HTML-Parsers namens Preloader zu minimieren.
Die Rolle eines Preloader-Scanners ist spekulativ. Das bedeutet, dass er das Roh-Markup untersucht, um Ressourcen zu finden, die opportunistisch abgerufen werden, bevor sie sonst vom primären HTML-Parser gefunden werden.
Prüfen, ob der Preloader funktioniert
Der Prefetch-Scanner wird aufgrund blockierter Rendering- und Parsevorgänge verwendet. Wenn diese beiden Leistungsprobleme nicht auftreten würden, wäre der Preloader nicht sehr nützlich. Ob eine Webseite vom Preloader profitiert, hängt von diesen Blockierungsphänomenen ab. Dazu können Sie eine künstliche Verzögerung für Anfragen einführen, um herauszufinden, wo der Preloader funktioniert.
Sehen wir uns als Beispiel diese Seite mit einfachem Text und Bildern mit einem Stylesheet an. Da CSS-Dateien sowohl das Rendering als auch das Parsen blockieren, wird durch einen Proxydienst eine künstliche Verzögerung von zwei Sekunden für das Stylesheet eingeführt. Durch diese Verzögerung ist in der Netzwerkabfolge leichter zu erkennen, wo der Preloader-Scanner aktiv ist.
Wie Sie in der Ablaufgrafik sehen, erkennt der Preloader das <img>
-Element auch dann, wenn das Rendering und das Parsen des Dokuments blockiert sind. Ohne diese Optimierung kann der Browser während des Blockierungszeitraums nicht opportunistisch Daten abrufen. Außerdem werden mehr Ressourcenanfragen nacheinander statt gleichzeitig gesendet.
Nachdem wir uns dieses Beispiel angesehen haben, sehen wir uns einige reale Muster an, bei denen der Preloader-Scanner ausgetrickst werden kann, und was dagegen unternommen werden kann.
Eingeschleuste async
-Scripts
Angenommen, Ihr <head>
enthält HTML-Code mit Inline-JavaScript:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Eingebettete Scripts haben standardmäßig den Wert async
. Wenn dieses Script eingebettet wird, verhält es sich so, als wäre das Attribut async
darauf angewendet worden. Das bedeutet, dass es so schnell wie möglich ausgeführt wird und das Rendering nicht blockiert. Klingt optimal, oder? Wenn Sie jedoch davon ausgehen, dass dieses Inline-<script>
nach einem <link>
-Element kommt, das eine externe CSS-Datei lädt, erhalten Sie ein suboptimales Ergebnis:
Sehen wir uns an, was hier passiert ist:
- Nach 0 Sekunden wird das Hauptdokument angefordert.
- Nach 1, 4 Sekunden wird das erste Byte der Navigationsanfrage empfangen.
- Nach 2, 0 Sekunden werden das CSS und das Bild angefordert.
- Da der Parser das Laden des Stylesheets blockiert und das Inline-JavaScript, das das
async
-Script einschleust, nach diesem Stylesheet mit 2,6 Sekunden kommt, sind die Funktionen des Scripts nicht so schnell verfügbar, wie sie es sein könnten.
Das ist nicht optimal, da die Anfrage für das Script erst erfolgt, nachdem der Download des Stylesheets abgeschlossen ist. Dadurch wird die Ausführung des Scripts verzögert. Da das <img>
-Element dagegen im vom Server bereitgestellten Markup sichtbar ist, wird es vom Preloader-Scanner erkannt.
Was passiert, wenn Sie ein reguläres <script>
-Tag mit dem async
-Attribut verwenden, anstatt das Script in das DOM einzuschleusen?
<script src="/yall.min.js" async></script>
Das Ergebnis sieht so aus:
Es kann verlockend sein, zu behaupten, dass diese Probleme mit rel=preload
behoben werden könnten. Das würde zwar funktionieren, kann aber einige Nebenwirkungen haben. Warum sollte man rel=preload
verwenden, um ein Problem zu beheben, das vermieden werden kann, indem man kein <script>
-Element in das DOM einschleust?
Durch das Vorladen wird das Problem hier zwar behoben, aber es entsteht ein neues: Das async
-Script in den ersten beiden Demos wird – obwohl es im <head>
geladen wird – mit der Priorität „Niedrig“ geladen, während das Stylesheet mit der Priorität „Höchste“ geladen wird. In der letzten Demo, in der das async
-Script vorab geladen wird, wird das Stylesheet weiterhin mit der Priorität „Höchste“ geladen, die Priorität des Scripts wurde jedoch auf „Hoch“ erhöht.
Wenn die Priorität einer Ressource erhöht wird, weist der Browser ihr mehr Bandbreite zu. Das bedeutet, dass die erhöhte Priorität des Scripts zu Bandbreitenkonflikten führen kann, auch wenn das Stylesheet die höchste Priorität hat. Das kann bei langsamen Verbindungen oder bei sehr großen Ressourcen ein Faktor sein.
Die Antwort ist einfach: Wenn ein Script beim Starten benötigt wird, sollten Sie den Preloader nicht durch Einschleusen in das DOM austricksen. Experimentieren Sie nach Bedarf mit der Platzierung von <script>
-Elementen sowie mit Attributen wie defer
und async
.
Lazy Loading mit JavaScript
Das Lazy-Loading ist eine gute Methode, um Daten zu sparen. Diese Methode wird häufig auf Bilder angewendet. Manchmal wird Lazy Loading jedoch fälschlicherweise auf Bilder angewendet, die sich sozusagen „above the fold“ befinden.
Dies kann zu potenziellen Problemen bei der Ressourcenerkennung durch den Preloader führen und die Zeit, die benötigt wird, um eine Referenz auf ein Bild zu finden, herunterzuladen, zu decodieren und zu präsentieren, unnötig verlängern. Sehen wir uns dieses Bild-Markup als Beispiel an:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Die Verwendung eines data-
-Präfixes ist ein gängiges Muster in JavaScript-basierten Lazy-Loadern. Wenn das Bild in den Viewport gescrollt wird, entfernt der Lazy-Loader das Präfix data-
. Im vorherigen Beispiel wird also data-src
zu src
. Dadurch wird der Browser aufgefordert, die Ressource abzurufen.
Dieses Muster ist erst dann problematisch, wenn es auf Bilder angewendet wird, die sich beim Start im Darstellungsbereich befinden. Da der Preloader das data-src
-Attribut nicht auf dieselbe Weise liest wie ein src
- (oder srcset
-)Attribut, wird die Bildreferenz nicht früher erkannt. Schlimmer noch: Das Bild wird erst nach dem Herunterladen, Kompilieren und Ausführen des Lazy-Loader-JavaScripts geladen.
Je nach Größe des Bildes, was auch von der Größe des Darstellungsbereichs abhängen kann, kann es ein Element für Largest Contentful Paint (LCP) sein. Wenn der Preloader die Bildressource nicht spekulativ im Voraus abrufen kann – möglicherweise zu dem Zeitpunkt, an dem die Stylesheets der Seite das Rendering blockieren –, verschlechtert sich der LCP.
Die Lösung besteht darin, das Bild-Markup zu ändern:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Das ist das optimale Muster für Bilder, die sich beim Start im Viewport befinden, da der Preloader die Bildressource schneller erkennt und abruft.
In diesem vereinfachten Beispiel führt dies zu einer Verbesserung des LCP um 100 Millisekunden bei einer langsamen Verbindung. Das mag nicht nach einer großen Verbesserung aussehen, aber das ist es, wenn Sie bedenken, dass die Lösung eine schnelle Korrektur des Markups ist und dass die meisten Webseiten komplexer sind als diese Beispiele. Das bedeutet, dass sich LCP-Kandidaten möglicherweise mit vielen anderen Ressourcen um die Bandbreite streiten müssen. Daher werden Optimierungen wie diese immer wichtiger.
CSS-Hintergrundbilder
Der Browser-Preload-Scanner scannt Markup. Andere Ressourcentypen wie CSS werden nicht gescannt. Das kann Abrufe von Bildern erfordern, auf die über die background-image
-Property verwiesen wird.
Wie HTML wird CSS von Browsern in ein eigenes Objektmodell verarbeitet, das als CSSOM bezeichnet wird. Wenn beim Erstellen des CSSOM externe Ressourcen gefunden werden, werden diese Ressourcen zum Zeitpunkt der Erkennung angefordert und nicht vom Pre-Load-Scanner.
Angenommen, der LCP-Kandidat Ihrer Seite ist ein Element mit einer CSS-background-image
-Eigenschaft. Beim Laden von Ressourcen geschieht Folgendes:
In diesem Fall wird der Preloader nicht so sehr umgangen, sondern spielt keine Rolle. Wenn ein LCP-Kandidat auf der Seite jedoch aus einer background-image
-CSS-Property stammt, sollten Sie dieses Bild vorab laden:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Diese rel=preload
-Hinweis ist klein, hilft dem Browser aber, das Bild früher zu finden als sonst:
Mit dem rel=preload
-Hinweis wird der LCP-Kandidat früher gefunden, wodurch sich die LCP-Zeit verkürzt. Dieser Hinweis hilft zwar, dieses Problem zu beheben, aber es ist möglicherweise besser, zu prüfen, ob Ihr LCP-Kandidat für Bilder aus CSS geladen werden muss. Mit einem <img>
-Tag haben Sie mehr Kontrolle über das Laden eines Bildes, das für den Darstellungsbereich geeignet ist, und ermöglichen gleichzeitig dem Preloader-Scanner, es zu finden.
Einfügen zu vieler Ressourcen
Beim Inline-Einfügen wird eine Ressource in den HTML-Code eingefügt. Sie können Stylesheets in <style>
-Elemente, Scripts in <script>
-Elemente und praktisch jede andere Ressource mithilfe der Base64-Codierung inline einbinden.
Das Einfügen von Ressourcen kann schneller sein als das Herunterladen, da keine separate Anfrage für die Ressource gestellt wird. Sie befindet sich direkt im Dokument und wird sofort geladen. Es gibt jedoch erhebliche Nachteile:
- Wenn Sie Ihr HTML nicht im Cache speichern – was bei dynamischen HTML-Antworten nicht möglich ist –, werden die Inline-Ressourcen nie im Cache gespeichert. Das wirkt sich auf die Leistung aus, da die eingefügten Ressourcen nicht wiederverwendet werden können.
- Auch wenn Sie HTML im Cache speichern können, werden eingebettete Ressourcen nicht zwischen Dokumenten geteilt. Dies reduziert die Caching-Effizienz im Vergleich zu externen Dateien, die für einen gesamten Ursprung im Cache gespeichert und wiederverwendet werden können.
- Wenn Sie zu viel Inline-Code verwenden, verzögert sich das Erkennen von Ressourcen später im Dokument, da das Herunterladen dieser zusätzlichen Inline-Inhalte länger dauert.
Nehmen wir diese Seite als Beispiel. Unter bestimmten Bedingungen ist das Bild oben auf der Seite der LCP-Kandidat und das CSS befindet sich in einer separaten Datei, die von einem <link>
-Element geladen wird. Die Seite verwendet außerdem vier Web-Schriftarten, die als separate Dateien von der CSS-Ressource angefordert werden.
Was passiert, wenn das CSS und alle Schriftarten als Base64-Ressourcen eingefügt werden?
Die Auswirkungen des Inline-Einfügens haben in diesem Beispiel negative Folgen für die LCP und die Leistung im Allgemeinen. Auf der Version der Seite, auf der nichts inline ist, wird das LCP-Bild in etwa 3,5 Sekunden dargestellt. Auf der Seite, auf der alles inline ist, wird das LCP-Bild erst nach etwas mehr als 7 Sekunden dargestellt.
Es geht hier nicht nur um den Preloader-Scanner. Das Einbetten von Schriftarten ist keine gute Strategie, da Base64 ein ineffizientes Format für binäre Ressourcen ist. Ein weiterer Faktor ist, dass externe Schriftressourcen nur heruntergeladen werden, wenn sie vom CSSOM als erforderlich eingestuft werden. Wenn diese Schriftarten als Base64-Inlining eingebunden sind, werden sie heruntergeladen, unabhängig davon, ob sie für die aktuelle Seite benötigt werden oder nicht.
Könnte ein Preloading die Situation verbessern? Sehr gern. Sie könnten das LCP-Bild vorab laden und die LCP-Zeit reduzieren. Das Aufblähen Ihres potenziell nicht im Cache ablegbaren HTML-Codes mit Inline-Ressourcen hat jedoch andere negative Auswirkungen auf die Leistung. Auch der First Contentful Paint (FCP) ist von diesem Muster betroffen. In der Version der Seite, in der nichts inline eingebunden ist, beträgt die FCP etwa 2,7 Sekunden. In der Version, in der alles inline ist, beträgt die FCP etwa 5,8 Sekunden.
Seien Sie vorsichtig, wenn Sie Inhalte in HTML einfügen, insbesondere Base64-codierte Ressourcen. Im Allgemeinen wird dies nicht empfohlen, es sei denn, es handelt sich um sehr kleine Ressourcen. Verwenden Sie Inline-Elemente so wenig wie möglich, da zu viele Inline-Elemente ein Sicherheitsrisiko darstellen.
Markup mit clientseitigem JavaScript rendern
Es besteht kein Zweifel: JavaScript wirkt sich definitiv auf die Seitenladezeit aus. Nicht nur Entwickler sind auf JavaScript angewiesen, um Interaktivität zu ermöglichen, sondern es gibt auch eine Tendenz, JavaScript für die Bereitstellung von Inhalten zu verwenden. Dies führt in gewisser Weise zu einer besseren Entwicklererfahrung. Vorteile für Entwickler bedeuten jedoch nicht immer Vorteile für Nutzer.
Ein Muster, das den Preloader-Scanner austricksen kann, ist das Rendern von Markup mit clientseitigem JavaScript:
Wenn Markup-Nutzlast in JavaScript im Browser enthalten und vollständig von JavaScript gerendert wird, sind alle Ressourcen in diesem Markup für den Preloader unsichtbar. Dadurch wird die Erkennung wichtiger Ressourcen verzögert, was sich auf die LCP auswirkt. In diesen Beispielen ist die Anfrage für das LCP-Bild erheblich verzögert, verglichen mit der entsprechenden servergerenderten Version, für die kein JavaScript erforderlich ist.
Das weicht ein wenig vom Schwerpunkt dieses Artikels ab, aber die Auswirkungen des Markup-Renderings auf dem Client gehen weit über das Verhindern des Preloading-Scanners hinaus. Zum einen führt die Einführung von JavaScript für eine Funktion, für die es nicht erforderlich ist, zu unnötigen Verarbeitungszeiten, die sich auf die Interaktion bis zur nächsten Darstellung (Interaction to Next Paint, INP) auswirken können. Beim Rendern extrem großer Mengen an Markup auf dem Client ist die Wahrscheinlichkeit höher, dass lange Aufgaben generiert werden, als wenn dieselbe Menge an Markup vom Server gesendet wird. Der Grund dafür ist neben der zusätzlichen Verarbeitung durch JavaScript, dass Browser Markup vom Server streamen und das Rendering so in kleine Teile aufteilen, dass lange Aufgaben in der Regel begrenzt werden. Clientseitig gerendertes Markup wird dagegen als einzelne, monolithische Aufgabe behandelt, was sich auf den INP einer Seite auswirken kann.
Die Lösung für dieses Szenario hängt von der Antwort auf diese Frage ab: Gibt es einen Grund, warum das Markup Ihrer Seite nicht vom Server bereitgestellt werden kann, sondern auf dem Client gerendert werden muss? Wenn die Antwort „Nein“ lautet, sollten Sie nach Möglichkeit serverseitiges Rendering (SSR) oder statisch generiertes Markup in Betracht ziehen. So kann der Preloader wichtige Ressourcen im Voraus finden und bei Bedarf abrufen.
Wenn für Ihre Seite JavaScript erforderlich ist, um einigen Teilen des Seiten-Markups Funktionen hinzuzufügen, können Sie dies auch mit SSR tun, entweder mit Vanilla-JavaScript oder mit Hydration, um das Beste aus beiden Welten zu nutzen.
Dem Preloader-Scanner helfen, Ihnen zu helfen
Der Preloader ist eine äußerst effektive Browseroptimierung, die das Laden von Seiten beim Starten beschleunigt. Wenn Sie Muster vermeiden, die die Fähigkeit von Lighthouse beeinträchtigen, wichtige Ressourcen im Voraus zu finden, vereinfachen Sie nicht nur die Entwicklung, sondern sorgen auch für eine bessere Nutzerfreundlichkeit, die zu besseren Ergebnissen bei vielen Messwerten führt, einschließlich einiger Web Vitals.
Hier noch einmal die wichtigsten Punkte aus diesem Beitrag:
- Der Browser-Preload-Scanner ist ein sekundärer HTML-Parser, der vor dem primären Parser scannt, wenn dieser blockiert ist, um Ressourcen zu finden, die früher abgerufen werden können.
- Ressourcen, die nicht im Markup vorhanden sind, das der Server bei der ersten Navigationsanfrage zur Verfügung stellt, können vom Pre-Load-Scanner nicht gefunden werden. Beispiele für Möglichkeiten, den Preloader-Scanner zu umgehen:
- Ressourcen mit JavaScript in das DOM einschleusen, z. B. Scripts, Bilder, Stylesheets oder andere Elemente, die besser in der ursprünglichen Markup-Nutzlast vom Server enthalten wären.
- Lazy Loading von Bildern oder iFrames im Above-the-Fold-Bereich mit einer JavaScript-Lösung
- Markup wird auf dem Client gerendert und kann Verweise auf untergeordnete Dokumentressourcen mit JavaScript enthalten.
- Der Scanner für das Vorabladen scannt nur HTML. Der Inhalt anderer Ressourcen, insbesondere CSS, wird nicht geprüft. Diese können Verweise auf wichtige Assets enthalten, einschließlich LCP-Kandidaten.
Wenn Sie aus irgendeinem Grund kein Muster vermeiden können, das die Fähigkeit des Preloader-Scanners, die Ladeleistung zu beschleunigen, negativ beeinträchtigt, sollten Sie den Ressourcenhinweis rel=preload
berücksichtigen. Wenn Sie rel=preload
verwenden, sollten Sie mithilfe von Lab-Tools testen, ob Sie den gewünschten Effekt erzielen. Laden Sie nicht zu viele Ressourcen vorab, denn wenn Sie alles priorisieren, wird nichts priorisiert.
Ressourcen
- Asynchrone Inline-Skripts vermeiden
- So sorgen Browser-Vorab-Lademechanismen für schnelleres Laden von Seiten
- Wichtige Assets vorab laden, um die Ladegeschwindigkeit zu verbessern
- Netzwerkverbindungen frühzeitig herstellen, um die wahrgenommene Seitengeschwindigkeit zu verbessern
- Largest Contentful Paint optimieren
Hero-Image von Unsplash, von Mohammad Rahmani