Eine enträtselte Performance-Geschichte, Teil 2: Render-blockierendes CSS

Benutzer auf einem Desktop sehen den Flash möglicherweise nur kurz, Benutzer mit einer langsamen mobilen Verbindung haben jedoch genügend Zeit, um Layoutinformationen auf der Seite zu analysieren. Wenn dann das CSS geladen wird, ändert sich das Seitenlayout, was zu einem desorientierten kognitiven Aufwand führt, der entsteht, wenn das, was angezeigt wird, neu analysiert werden muss.

Es besteht auch die große Wahrscheinlichkeit, dass sie versuchen, einen Link oder eine Schaltfläche zu drücken, wodurch sich die Position ändert, wenn der CSS-Code geladen wird. Dadurch würde der Benutzer nichts drücken, oder noch schlimmer, eine völlig andere Aktion.

Wir haben alle diese frustrierende Erfahrung gemacht, als eine Anzeige oder ein Bild den Inhalt herumwirbelt (dich ansieht, TFL).

Daher ist es sinnvoll, dass Browser CSS als Renderblocker behandeln, um diese Situationen zu vermeiden. Dies kann jedoch dazu führen, dass Benutzer bei langsameren Verbindungen länger auf das Lesen ihrer Inhalte warten müssen.

Die Situation auf DriveTribe

Unsere Analyse in Teil 1 ergab zwei CSS-Dateien, die das Rendern blockieren:

  • Google Fonts : Definiert eine Reihe von Webfonts und die Bedingungen, unter denen sie geladen werden.
  • Globales Stylesheet : Ein 32-KB-Stylesheet, das Stile für die gesamte Website enthält.

Wie aus dem Wasserfalldiagramm von WebPageTest hervorgeht, werden diese Assets auch aus verschiedenen Domänen bereitgestellt:

Die türkisfarbenen, orangefarbenen und rosa Balken sind DNS- und SSL-Overhead.

Viele Benutzer verbringen mehr Zeit mit dem Herstellen einer Verbindung zu den Servern , von denen diese Dateien bereitgestellt werden, als mit dem Herunterladen der Dateien.

Wenn wir herausfinden können, wie wir diese beiden Anforderungen umgehen können, sparen wir mindestens 100 ms (viel mehr bei langsamen Verbindungen).

Dies klingt möglicherweise nicht Wie viel Zeit, aber im Idealfall würden wir eine lesbare Website für einen mobilen Benutzer innerhalb einer Sekunde bereitstellen. Dies gibt uns schnell eine neue Perspektive auf diese Zahl.

Schauen wir uns also beide Dateien an und sehen, wie wir sie entfernen können.

Google Fonts

Die Standardmethode, die Google Fonts zum Einbetten seiner Schriftarten bereitstellt, ist eine blockierende CSS-Ressource (dies ist die von uns implementierte Methode):

   

Dieser Link verweist auf eine winzige CSS-Datei, die eine Reihe von @font-face Regeln enthält, die wie folgt aussehen:

  /* Latein */ 
@Schriftart {
Schriftfamilie: 'Open Sans';
Schriftstil: normal;
Schriftgröße: 400;
src: local ('Open Sans Regular'), local ('OpenSans-Regular'), url (https://fonts.gstatic.com/s/opensans/v15/cJZKeOuBrn4kERxqtaUH3ZBw1xU1rKptJj_0jans920.woff2) format ('woff2';
Unicode-Bereich: U + 0000-00FF, U + 0131, U + 0152-0153, U + 02C6, U + 02DA, U + 02DC, U + 2000-206F, U + 2074, U + 20AC, U + 2212, U + 2215;
}

In diesem unicode-range teilt die unicode-range dem Browser mit, dass beim Erkennen eines Zeichens innerhalb des angegebenen Bereichs die Datei heruntergeladen werden soll, auf die verwiesen wird.

Es ist im Wesentlichen eine Code-Aufteilung für Schriftarten, die es Web-Schriftarten ermöglicht, eine Reihe internationaler Zeichensätze zu unterstützen und gleichzeitig die Nutzlast für den Benutzer zu minimieren.

Eine vollständige DNS-Auflösung wirkt wie ein Overkill, wenn eine winzige Datei mit Schriftdefinitionen empfangen wird (die Datei ist mit einem GZIP von weniger als 1 KB versehen). Vor allem, wenn die Schriftarten selbst von einer separaten Domain bereitgestellt werden und eine weitere DNS-Auflösung erforderlich ist (!)

Potentielle Lösungen

Die erste mögliche Lösung, die in den Sinn kommt, besteht darin, diese Datei einfach zu kopieren und als Teil unserer anfänglichen HTML-Nutzdaten in ein style Tag einzufügen.

Dies wäre ein Ansatz, bei dem alle anderen Dinge gleich sind und unsere Ladestrategie bis auf eine Blockierungs-Netzwerkanforderung weniger genau gleich bleibt.

Ich kann mir zwei mögliche Probleme vorstellen:

1. Manuelle Referenzen pflegen

Wir würden jetzt einen manuellen Verweis auf die Schriftdateien selbst beibehalten. Das fühlt sich spröde an; Ich habe keine Ahnung, ob Google diese Dateien wahrscheinlich entfernen wird, wenn eine neue Version hinzugefügt wird.

Außerdem hat jede Datei eine ID wie cJZKeOuBrn4kERxqtaUH3ZBw1xU1rKptJj_0jans92 . Um die neue zu erhalten, muss der Betreuer diese CSS-Datei manuell aufrufen, anstatt eine fundierte Vermutung anzustellen.

Andererseits bedeutet die Tatsache, dass diese Schriftdateien instinktiv versioniert werden, dass es unwahrscheinlich (aber möglich) ist, dass Google frühere Versionen löscht.

2. Flash von unsichtbarem Text

Gegenwärtig behandeln Browser die Schriftarten selbst als semi-blockierende Ressource. Dies bedeutet, dass sie in der Regel eine Zeit lang (nach ihrer Wahl) auf das Laden der Schriftart warten, bevor sie auf eine Systemschriftart zurückgreifen.

Dies führt zu einem unsichtbaren Textblitz (FOIT). Die Seite wird gerendert, aber der Text ist nicht sichtbar. Unser aktueller Ansatz führt zu FOIT.

Eine mögliche Alternative besteht darin, dem Browser zunächst die Verwendung einer Ersatzschrift zu ermöglichen und diese nach dem erfolgreichen Laden durch die Webschrift zu ersetzen.

Dies führt zu einem Blitz aus nicht gestyltem Text (FOUT), der chaotisch aussieht, aber keinen größeren kognitiven Aufwand verursacht. Es gibt jedoch immer noch eine geringe Menge an Reflow, was beim Laden mehrerer Schriftarten die Leistung beeinträchtigt.

FOUT reduziert auch die zusätzliche Zeit, die ein Benutzer warten muss, bis Text angezeigt wird, von bis zu 30 Sekunden auf einen Wert in der Größenordnung von Millisekunden .

Alternativen

Zach Leatherman hat eine exzellente Übersicht über alle Strategien zum Laden von Web-Schriften in seinem Blog. Jeder hat eine umfangreiche Liste von Vor- und Nachteilen. Unter dem Strich gibt es derzeit keine einheitliche Lösung für Web-Schriftarten.

Einer der vielversprechendsten Ansätze ist “FOUT with a class”. Hierbei wird JavaScript zum asynchronen Laden der Schriftarten verwendet. Wenn alle Schriften heruntergeladen wurden, wird dem html Tag eine Klasse hinzugefügt, die die Web-Schriften über CSS anwendet.

Auf der positiven Seite können wir Inhalte sofort anzeigen. Wir bündeln auch die mehrfachen Rückflüsse in nur einen.

Wir laden jedoch immer noch eine externe Abhängigkeit herunter, nur um die Schriftarten zu laden. Der in diesem Artikel zu CSS-Tricks empfohlene Web Font Loader hat eine Größe von 16 KB. Das ist größer als eine typische JavaScript-Bibliothek und ungefähr so ​​groß wie eine unserer Schriftdateien!

Es würde uns auch nicht erlauben, Schriften weiterhin als Unicode-Bereiche zu definieren, ein großer Vorteil der CSS-Regeln von vanilla @font-face .

Schrift-Anzeige

Zachs Blog-Post erinnerte mich an die CSS-Eigenschaft zur font-display von font-display . Wir verwenden dies bereits für unsere lokal gehosteten Markenschriften, aber die externe CSS-Datei von Google lässt es weg.

font-display können Entwickler ihre eigene Strategie für die Anzeige von Inhalten definieren, während Webfonts geladen werden. Die Browserunterstützung ist derzeit sehr gut, nur bei Microsoft-Browsern fehlt sie.

Die font-display: fallback Regel ist ein Kompromiss zwischen dem hässlichen FOUT und dem langsamen, unfreundlichen FOIT. Das Rendern von Text mit einer Ersatzschrift dauert etwas länger (ca. 100 ms). Wenn die Webschrift danach geladen wird, tritt ein FOUT auf.

Benutzer mit einer schnellen Verbindung bemerken keine FOUT, während Benutzer mit einer langsamen Verbindung ihre Inhalte früher lesen können.

Das einzige verbleibende Hindernis für das Inlinen des von der externen Google-Datei bereitgestellten CSS besteht darin, Links zu den Schriftartdateien auf den Servern von Google zu verwalten.

Und weißt du was? Scheiß drauf. Meiner Meinung nach erreicht dieser Ansatz die beste Ausgewogenheit und ist voll und ganz Standard. Keine Hacks oder Polyfills, kein JavaScript. Wenn wir die Dateien lokal hosten würden, wäre dies definitiv die Lösung, die ich wählen würde. Es hat also keinen Wert, sich Gedanken darüber zu machen, was wäre wenn. Lass es uns versuchen. Ich werde diesen Artikel aktualisieren, wenn ich verbrannt werde.

Was uns mit der endgültigen Render-Blocking-Datei belässt …

Globales Stylesheet

Die Mehrheit der Websites stützt sich auf mindestens eine externe CSS-Datei. Unsere ist eine 32 KB große globale CSS-Datei, die Stile für die gesamte Website enthält.

Hier ist das Ding: Die Homepage braucht es nicht. Oder zumindest wahrscheinlich nicht. Wie ich in meiner Erklärung erklärt habe, warum wir zu gestylten Komponenten wechseln, zieht eine Hauptattraktion in das versprochene Land, in dem nur die Stile geladen werden, die zum Rendern der aktuellen Ansicht erforderlich sind.

Alle für die Homepage erforderlichen Stile sind derzeit mit der anfänglichen HTML-Nutzlast versehen. Das einzige potenzielle Problem besteht darin, sicherzustellen, dass ältere Komponenten wie das Warnungsmenü alle auf Gestaltete Komponenten portiert werden.

Dies wird mit ziemlicher Sicherheit brechen

Da Komponenten mit fehlenden Stilen im Hintergrund visuelle Fehler erben, wird 1) das globale Stylesheet entfernt und 2) jede mögliche Aktion auf der Seite überprüft, um sicherzustellen, dass alle ausgeblendeten Dialogfelder korrekt portiert und formatiert sind.

Weiterfahrten

Natürlich benötigt der Rest der Site dieses globale Stylesheet, daher benötigen wir eine Strategie zum Laden.

Wir könnten das globale Stylesheet möglicherweise aktivieren, indem wir es in jede andere Komponente mit React Helmet aufnehmen.

   

Dies würde das global.css Tag nur in Seiten global.css , die es benötigen.

Meine Besorgnis über diesen Ansatz rührt daher, dass wir in erster Linie hier sind: Browser behandeln CSS als Ressource, die das Rendern blockiert. Mit React Helmet kann ein Benutzer auf einer Seite landen, die kein CSS enthält. Wenn sie zu einer Route navigieren, die dies tut, muss React sie rendern, bevor wir überhaupt wissen, dass wir sie anfordern müssen .

Dies wird mit ziemlicher Sicherheit zu einem Flash mit nicht gestalteten Inhalten führen.

Die Lösung

Jede Route in DriveTribe wird durch eine Routendefinitionsdatei konfiguriert. Es enthält Datenabhängigkeiten, eine Root-Komponente, eine Pfadgenerierungsfunktion und eine Reihe anderer Einstellungen. Es sieht ungefähr so ​​aus:

  const chatRoute: RouteDefinition = { 
Pfad: (id: string) => `` / chat / $ {id} `,
view: () => import ( / * webpackChunkName: "chat" * / './Chat'),
Daten: {
check: (state, {params}) => getChat (state, params.resourceId),
fetch: ({params}) => getChatRequest (params.resourceId)
}
};

Alle main.js sind in main.js (dies ist eine mögliche Optimierung für einen späteren Artikel). Üblicherweise definiert eine React-Komponente ihre eigenen Datenabhängigkeiten. Dies ist unter anderem der Ansatz von Next.js.

Dies bedeutet jedoch, dass eine Komponente heruntergeladen und analysiert werden muss, bevor die Daten abgerufen werden können. Mit diesem Ansatz für Konfigurationsdateien können wir die Daten parallel zur Komponente abrufen.

Ich werde RouteDefinition einen neuen Parameter RouteDefinition , isStylesheetBlocking?: true . Als Legacy-Konfiguration füge ich es allen Routen außer der Homepage hinzu.

Dann füge ich in die HTML-Vorlage den folgenden Code ein:

  isStylesheetBlocking 
? ``
: ``;

Auf Seiten, die dies erfordern, wird das Stylesheet weiterhin als blockierende Ressource geladen.

Auf neueren Seiten können wir das Tag , um die CSS-Datei asynchron abzurufen.

Das onload="this.rel='stylesheet'" attribute ist ein winziger JS-Befehl, den das Tag verwenden kann, um sich nach dem Laden des CSS in eine reguläre stylesheet Ressource zu konvertieren.

rel="preload" bietet, wie so oft im Leben, keine breite Browserunterstützung. Stattdessen werde ich loadCSS am unteren Rand der HTML-Vorlage einbinden. Dieses Mikroskript von Filament Group erweitert die Funktionalität und lädt die CSS-Synchronisierung wie gewünscht.

Letztendlich bedeutet dieser Ansatz, dass wir keine verbleibenden Komponenten portieren müssen, die für Styled Components nicht sofort sichtbar sind. Da das globale CSS letztendlich noch geladen ist, ist es unwahrscheinlich, dass die Stile für ausgeblendete Dialoge nach dem JavaScript geladen werden, das die Dialoge selbst antreibt.