Zum Hauptinhalt springen




Wie wir Code-Splitting, Code-Inlining und serverseitiges Rendern in PROXX verwendet haben.

At Google I/O 2019 Mariko, Jake, and I shipped PROXX, a modern Minesweeper-clone for the Netz. Something that sets PROXX apart is the focus on accessibility (you can play it with a screenreader!) and the ability to run as well on a feature phone as on a high-end desktop device. Feature phones are constrained in multiple ways:

  • Schwache CPUs
  • Schwache oder nicht vorhandene GPUs
  • Kleine Bildschirme ohne Berührungseingabe
  • Sehr begrenzte Speichermengen

Aber sie betreiben einen modernen Browser und sind sehr erschwinglich. Aus diesem Grund erleben Feature-Telefone in Schwellenländern eine Wiederbelebung. Ihr Preis ermöglicht es einem ganz neuen Publikum, das es sich bisher nicht leisten konnte, online zu gehen und das moderne Web zu nutzen. Für 2019 wird prognostiziert, dass allein in Indien rund 400 Millionen Feature-Telefone verkauft werdenDaher werden Benutzer auf Funktionstelefonen möglicherweise zu einem bedeutenden Teil Ihres Publikums. Darüber hinaus sind Verbindungsgeschwindigkeiten ähnlich 2G in Schwellenländern die Norm. Wie haben wir es geschafft, dass PROXX unter den Bedingungen eines Feature-Telefons gut funktioniert?

PROXX Gameplay.

Performance is important, and that includes both loading performance and runtime performance. It has been shown that Eine gute Leistung korreliert mit einer erhöhten Benutzerbindung, verbesserten Conversions und vor allem einer erhöhten Inklusivität. Jeremy Wagner has much more data and Einblick auf warum Leistung wichtig ist.

Dies ist Teil 1 einer zweiteiligen Serie. Teil 1 konzentriert sich auf die LadeleistungTeil 2 konzentriert sich auf die Laufzeitleistung.

Erfassen des Status Quo

Testen Sie Ihre Ladeleistung auf einem echt Gerät ist kritisch. Wenn Sie kein echtes Gerät zur Hand haben, empfehle ich WebPageTest (WPT), speziell die "Einfache" Einrichtung. WPT führt eine Reihe von Ladetests auf einem aus echt Gerät mit emulierter 3G-Verbindung.

3G ist eine gute Messgeschwindigkeit. Während Sie vielleicht an 4G, LTE oder bald sogar an 5G gewöhnt sind, sieht die Realität des mobilen Internets ganz anders aus. Vielleicht sind Sie in einem Zug, auf einer Konferenz, auf einem Konzert oder auf einem Flug. Was Sie dort erleben werden, ist höchstwahrscheinlich näher an 3G und manchmal sogar noch schlimmer.

That being said, we’re going to focus on 2G in this article because PROXX is explicitly Targeting feature phones and emerging markets in its Ziel audience. Once WebPageTest has run its test, you get a waterfall (equivalente to what you see in DevTools) as well as a filmstrip at the top. The film strip shows what your user sees while your App is loading. On 2G, the loading experience of the unoptimized version of PROXX is pretty bad:

Das Filmstreifenvideo zeigt, was der Benutzer sieht, wenn PROXX über eine emulierte 2G-Verbindung auf ein echtes Low-End-Gerät geladen wird.

Beim Laden über 3G sieht der Benutzer 4 Sekunden weißes Nichts. Über 2G sieht der Benutzer über 8 Sekunden absolut nichts. Wenn du liest warum Leistung wichtig ist you know that we have now lost a good portion of our potential users due to impatience. The user needs to download all of the 62 KB of JavaScript for anything to appear on screen. The silver lining in this scenario is that the second anything appears on screen it is also interactive. Or is it?

proxx-first-render-4341069

Die erste sinnvolle Farbe in der nicht optimierten Version von PROXX ist technisch interaktiv, aber für den Benutzer nutzlos.

Nachdem ungefähr 62 KB gzip'd JS heruntergeladen und das DOM generiert wurden, kann der Benutzer unsere App sehen. Die App ist technisch interaktiv. Ein Blick auf das Visuelle zeigt jedoch eine andere Realität. Die Web-Schriftarten werden immer noch im Hintergrund geladen, und bis sie fertig sind, kann der Benutzer keinen Text sehen. Während dieser Status als First Meaningful Paint (FMP) qualifiziert ist, gilt er sicherlich nicht als ordnungsgemäß interaktiv, da der Benutzer nicht erkennen kann, worum es bei den Eingaben geht. Bei 3G dauert es eine weitere Sekunde und bei 2G 3 Sekunden, bis die App betriebsbereit ist. Insgesamt benötigt die App 6 Sekunden für 3G und 11 Sekunden für 2G, um interaktiv zu werden.

Wasserfallanalyse

Jetzt wo wir es wissen que Der Benutzer sieht, wir müssen das herausfinden Warum. Dazu können wir uns den Wasserfall ansehen und analysieren, warum Ressourcen zu spät geladen werden. In unserem 2G-Trace für PROXX sehen wir zwei große rote Fahnen:

  1. Es gibt mehrere mehrfarbige dünne Linien.
  2. JavaScript-Dateien bilden eine Kette. Beispielsweise wird die zweite Ressource erst geladen, wenn die erste Ressource fertig ist, und die dritte Ressource startet erst, wenn die zweite Ressource fertig ist.
waterfall_opt-8683308
Der Wasserfall gibt Aufschluss darüber, welche Ressourcen wann und wie lange geladen werden.

Verbindungsanzahl reduzieren

Jede dünne Linie (DNS, verbinden, ssl) stands for the creation of a new HTTP connection. Setting up a new connection is costly as it takes around 1s on 3G and roughly 2.5s on 2G. In our waterfall we see a new connection for:

  • Anfrage #1: Unsere Index.html
  • Anfrage #5: Die Schriftstile von fonts.googleapis.com
  • Request #8: Google Analytics
  • Anfrage #9: Eine Schriftartdatei von fonts.gstatic.com
  • Anforderung #14: Das Web-App-Manifest

Die neue Verbindung für index.html ist unvermeidlich. Der Browser hast du um eine Verbindung zu unserem Server herzustellen, um den Inhalt zu erhalten. Die neue Verbindung für Google Analytics könnte vermieden werden, indem so etwas eingefügt wird Minimale AnalyseGoogle Analytics verhindert jedoch nicht, dass unsere App gerendert wird oder interaktiv wird. Daher ist es uns egal, wie schnell sie geladen wird. Im Idealfall sollte Google Analytics im Leerlauf geladen werden, wenn alles andere bereits geladen wurde. Auf diese Weise wird beim ersten Laden weder Bandbreite noch Rechenleistung beansprucht. Die neue Verbindung für das Web-App-Manifest lautet vorgeschrieben durch die Abrufspezifikation, da das Manifest über eine Verbindung ohne Berechtigungsnachweis geladen werden muss. Auch hier verhindert das Web-App-Manifest nicht, dass unsere App gerendert oder interaktiv wird, sodass wir uns nicht so sehr darum kümmern müssen.

The two fonts and their styles, however, are a problem as they block rendering and also interactivity. If we look at the CSS that is delivered by fonts.googleapis.comEs sind nur zwei @ Schriftart Regeln, eine für jede Schriftart. Die Schriftart Stile are so small in fact, that we decided to inline it into our HTML, removing one unnecessary connection. To avoid the cost of the connection setup for the font Dateien, we can Kopieren them to our own server.

Hinweis: Das Kopieren von CSS- oder Schriftdateien auf Ihren eigenen Server ist bei Verwendung in Ordnung Google Fonts. Andere Schriftartenanbieter haben möglicherweise andere Regeln. Bitte erkundigen Sie sich bei den Nutzungsbedingungen Ihres Schriftanbieters!

Lasten parallelisieren

Wenn wir uns den Wasserfall ansehen, können wir sehen, dass nach dem Laden der ersten JavaScript-Datei sofort neue Dateien geladen werden. Dies ist typisch für Modulabhängigkeiten. Unser Hauptmodul verfügt wahrscheinlich über statische Importe, sodass JavaScript erst ausgeführt werden kann, wenn diese Importe geladen sind. Das Wichtigste dabei ist, dass diese Arten von Abhängigkeiten zum Zeitpunkt der Erstellung bekannt sind. Wir können davon Gebrauch machen <enlace rel="preload"> Tags, um sicherzustellen, dass alle Abhängigkeiten in dem Moment geladen werden, in dem wir unseren HTML-Code erhalten.

Ergebnisse

Werfen wir einen Blick darauf, was unsere Änderungen erreicht haben. Es ist wichtig, keine anderen Variablen in unserem Testaufbau zu ändern, die die Ergebnisse verzerren könnten, daher werden wir sie verwenden Einfache Einrichtung von WebPageTest für den Rest dieses Artikels und schauen Sie sich den Filmstreifen an:

Wir verwenden den Filmstreifen von WebPageTest, um zu sehen, was unsere Änderungen erreicht haben.

Diese Änderungen reduzierten unseren TTI von 11 auf 8,5Dies entspricht ungefähr den 2,5 Sekunden der Verbindungsaufbauzeit, die wir entfernen wollten. Gut gemacht uns.

Vorrendern

Während wir gerade unseren TTI reduziert haben, haben wir den ewig langen weißen Bildschirm, den der Benutzer 8,5 Sekunden lang aushalten muss, nicht wirklich beeinflusst. Wohl the biggest improvements for FMP can be achieved by sending styled Markup in deinem index.html. Übliche Techniken, um dies zu erreichen, sind Vorrendern und serverseitiges Rendern, die eng miteinander verbunden sind und in erläutert werden Rendern im Web. Beide Techniken führen die Web-App in Node aus und serialisieren das resultierende DOM in HTML. Das serverseitige Rendern erledigt dies pro Anforderung auf der Serverseite, während das Vorrendern dies zur Erstellungszeit tut und die Ausgabe als Ihre neue speichert index.html. Da ist PROXX ein JAMStack App und hat keine Server-Seite, haben wir beschlossen, Prerendering zu implementieren.

Es gibt viele Möglichkeiten, einen Prerenderer zu implementieren. In PROXX haben wir uns für die Verwendung entschieden Puppenspieler, which starts Chrome without any UI and allows you to remote control that instance with a Node API. We use this to inject our markup and our JavaScript and then read back the DOM as a string of HTML. Because we are using CSS-ModuleWir erhalten CSS-Inlining der Stile, die wir kostenlos benötigen.

  const Browser = erwarten puppeteer.launch();
const Seite = erwarten Browser.newPage();
erwarten Seite.setContent(rawIndexHTML);
erwarten Seite.evaluate(codeToRun);
const renderedHTML = erwarten Seite.Inhalt();
Browser.close();
erwarten writeFile("index.html", renderedHTML);

Damit können wir eine Verbesserung für unseren FMP erwarten. Wir müssen immer noch die gleiche Menge an JavaScript wie zuvor laden und ausführen, daher sollten wir nicht erwarten, dass sich TTI stark ändert. Wenn überhaupt, unsere index.html ist größer geworden und könnte unseren TTI etwas zurückschieben. Es gibt nur einen Weg, dies herauszufinden: Ausführen von WebPageTest.

Der Filmstreifen zeigt eine deutliche Verbesserung unserer FMP-Metrik. TTI ist meist nicht betroffen.

Unsere erste aussagekräftige Farbe hat sich von 8,5 Sekunden auf 4,9 Sekunden bewegt. eine massive Verbesserung. Unser TTI liegt immer noch bei etwa 8,5 Sekunden, sodass er von dieser Änderung weitgehend unberührt blieb. Was wir hier gemacht haben, ist a Wahrnehmung Veränderung. Einige nennen es vielleicht sogar ein Kunststück. Indem wir ein Zwischenbild des Spiels rendern, ändern wir die wahrgenommene Ladeleistung zum Besseren.

Inlining

Eine weitere Metrik, die uns sowohl DevTools als auch WebPageTest geben, ist Time To First Byte (TTFB). Dies ist die Zeit, die vom ersten Byte der gesendeten Anforderung bis zum ersten Byte der empfangenen Antwort benötigt wird. Diese Zeit wird oft auch als Round Trip Time (RTT) bezeichnet, obwohl technisch gesehen ein Unterschied zwischen diesen beiden Zahlen besteht: RTT enthält nicht die Verarbeitungszeit der Anforderung auf der Serverseite. DevTools und WebPageTest visualisieren TTFB mit einer hellen Farbe innerhalb des Anforderungs- / Antwortblocks.

Der helle Abschnitt einer Anfrage zeigt an, dass die Anfrage auf den Empfang des ersten Bytes der Antwort wartet.

Wenn wir unseren Wasserfall betrachten, können wir sehen, dass die Alle Anfragen geben die aus Mehrheit ihrer Zeit warten für das erste Byte der Antwort ankommen.

Für dieses Problem wurde HTTP / 2 Push ursprünglich konzipiert. Der App-Entwickler weiß dass bestimmte Ressourcen benötigt werden und können drücken sie den Draht hinunter. Wenn der Client erkennt, dass er zusätzliche Ressourcen abrufen muss, befinden sie sich bereits in den Caches des Browsers. HTTP / 2 Push erwies sich als zu schwierig, um es richtig zu machen, und wird als entmutigt angesehen. Dieser Problembereich wird während der Standardisierung von HTTP / 3 erneut überprüft. Zur Zeit, Die einfachste Lösung ist zu im Einklang alle kritischen Ressourcen auf Kosten der Caching-Effizienz.

Unser kritisches CSS ist dank CSS-Modulen und unserem Puppenspieler-basierten Prerenderer bereits integriert. Für JavaScript müssen wir unsere kritischen Module einbinden und ihre Abhängigkeiten. Diese Aufgabe hat je nach verwendetem Bundler unterschiedliche Schwierigkeiten.

Hinweis: In this step we also subset our font files to contain only the glyphs that we need for our Zielseite. I am not going to go into detail on this step as it is not easily abstracted and sometimes not even practical. We still load the full font files lazily, but they are not needed for the initial render.

Mit dem Inlining unseres JavaScript haben wir unseren TTI von 8,5 auf 7,2 Sekunden reduziert.

Dies hat unseren TTI um 1 Sekunde verkürzt. Wir haben jetzt den Punkt erreicht, an dem unsere index.html enthält alles, was für das anfängliche Rendern und Interaktivieren benötigt wird. Der HTML-Code kann gerendert werden, während er noch heruntergeladen wird, wodurch unser FMP erstellt wird. Sobald der HTML-Code analysiert und ausgeführt wurde, ist die App interaktiv.

Aggressive Code-Aufteilung

Ja, unsere index.html enthält alles, was benötigt wird, um interaktiv zu werden. Bei näherer Betrachtung stellt sich jedoch heraus, dass es auch alles andere enthält. Unsere index.html ist ungefähr 43 KB. Lassen Sie uns dies in Bezug auf das setzen, mit dem der Benutzer zu Beginn interagieren kann: Wir haben ein Formular zum Konfigurieren des Spiels, das einige Komponenten, eine Startschaltfläche und wahrscheinlich Code zum Beibehalten und Laden von Benutzereinstellungen enthält. Das wars so ziemlich. 43 KB scheinen viel zu sein.

proxx-geladen-1500619
Die Landingpage von PROXX. Hier werden nur kritische Komponenten verwendet.

Um zu verstehen, woher unsere Bundle-Größe kommt, können wir a verwenden Quellkarten-Explorer or a equivalente tool to break down what the bundle consists of. As predicted, our bundle contains the game logic, the rendering engine, the win screen, the lose screen and a bunch of utilities. Only a small subset of these modules are needed for the landing page. Moving everything that is not strictly required for interactivity into a lazily-loaded module will decrease TTI bedeutend.

Die Analyse des Inhalts der PROXX-Datei "index.html" zeigt viele nicht benötigte Ressourcen. Kritische Ressourcen werden hervorgehoben.

Was wir tun müssen, ist Code-Split. Durch das Aufteilen von Code wird Ihr monolithisches Bündel in kleinere Teile zerlegt, die bei Bedarf faul geladen werden können. Beliebte Bundler mögen Webpack, Aufrollen, und Paket Unterstützung der Code-Aufteilung mithilfe von Dynamic importieren (). Der Bundler analysiert Ihren Code und im Einklang alle Module, die importiert werden statisch. Alles, was Sie importieren dynamisch wird in eine eigene Datei gestellt und erst dann aus dem Netzwerk abgerufen, wenn die importieren () Aufruf wird ausgeführt. Natürlich ist das Erreichen des Netzwerks mit Kosten verbunden und sollte nur durchgeführt werden, wenn Sie Zeit haben. Das Mantra hier besteht darin, die vorhandenen Module statisch zu importieren kritisch benötigt zum Ladezeitpunkt und lädt dynamisch alles andere. Sie sollten jedoch nicht bis zum letzten Moment warten, um Module zu laden, die definitiv verwendet werden. Phil Walton's Leerlauf bis dringend ist ein großartiges Muster für einen gesunden Mittelweg zwischen faulem Laden und eifrigem Laden.

In PROXX haben wir eine erstellt faul.js Datei, die statisch alles importiert, was wir Nein brauchen. In unserer Hauptdatei können wir dann dynamisch importieren faul.js. Einige unserer Vorbereiten Komponenten endeten in faul.jsDies stellte sich als etwas kompliziert heraus, da Preact nicht mit faul geladenen Komponenten sofort umgehen kann. Aus diesem Grund haben wir ein wenig geschrieben aufgeschoben Komponenten-Wrapper, mit dem wir einen Platzhalter rendern können, bis die eigentliche Komponente geladen wurde.

Export Standard Funktion aufgeschoben(componentPromise) {
Rückkehr Klasse Deferred extends Component {
Baumeister(props) {
super(props);
Este.Zustand = {
LoadedComponent: nicht definiert
};
componentPromise.then(component => {
Este.setState({ LoadedComponent: component });
});
}

render({ loaded, Wird geladen }, { LoadedComponent }) {
wenn (LoadedComponent) {
Rückkehr loaded(LoadedComponent);
}
Rückkehr Wird geladen();
}
};
}

Wenn dies vorhanden ist, können wir ein Versprechen einer Komponente in unserem verwenden render () Funktionen. Zum Beispiel die Komponente, die das animierte Hintergrundbild rendert, wird durch eine leere ersetzt <div> während die Komponente geladen wird. Sobald die Komponente geladen und einsatzbereit ist, wird die <div> wird durch die eigentliche Komponente ersetzt.

const NebulaDeferred = aufgeschoben(
importieren("/components/nebula").then(m => m.Standard)
);

Rückkehr (
<NebulaDeferred
Wird geladen={() => <div />}
loaded={Nebula => <Nebula />}
/>
);

Mit all dem haben wir unsere reduziert index.html auf nur 20 KB, weniger als die Hälfte der Originalgröße. Wie wirkt sich das auf FMP und TTI aus? WebPageTest wird es zeigen!

Der Filmstreifen bestätigt: Unser TTI liegt jetzt bei 5,4 Sekunden. Eine drastische Verbesserung gegenüber unseren ursprünglichen 11ern.

Unser FMP und TTI sind nur 100 ms voneinander entfernt, da es nur darum geht, das inline-JavaScript zu analysieren und auszuführen. Nach nur 5,4 Sekunden auf 2G ist die App vollständig interaktiv. Alle anderen, weniger wichtigen Module werden im Hintergrund geladen.

Mehr Fingerspitzengefühl

Wenn Sie sich unsere Liste der kritischen Module oben ansehen, werden Sie feststellen, dass die Rendering-Engine nicht Teil der kritischen Module ist. Natürlich kann das Spiel erst gestartet werden, wenn wir unsere Rendering-Engine haben, um das Spiel zu rendern. Wir könnten die Schaltfläche «Start» deaktivieren, bis unsere Rendering-Engine zum Starten des Spiels bereit ist. Nach unserer Erfahrung benötigt der Benutzer jedoch normalerweise lange genug, um seine Spieleinstellungen so zu konfigurieren, dass dies nicht erforderlich ist. Meistens werden die Rendering-Engine und die anderen verbleibenden Module geladen, wenn der Benutzer «Start» drückt. In dem seltenen Fall, dass der Benutzer schneller als seine Netzwerkverbindung ist, wird ein einfacher Ladebildschirm angezeigt, der darauf wartet, dass die verbleibenden Module fertig sind.

Fazit

Messen ist wichtig. Um zu vermeiden, dass Sie sich mit Problemen befassen, die nicht real sind, empfehlen wir, immer zuerst zu messen, bevor Sie Optimierungen implementieren. Zusätzlich sollten Messungen am durchgeführt werden echt Geräte an einer 3G-Verbindung oder an WebPageTest wenn kein echtes Gerät zur Hand ist.

Der Filmstreifen kann einen Einblick in das Laden Ihrer App geben fühlt sich für den Benutzer. Der Wasserfall kann Ihnen sagen, welche Ressourcen für möglicherweise lange Ladezeiten verantwortlich sind. Hier ist eine Checkliste mit Maßnahmen, mit denen Sie die Ladeleistung verbessern können:

  • Stellen Sie so viele Assets wie möglich über eine Verbindung bereit.
  • Laden Sie Ressourcen vor oder sogar inline, die für das erste Rendern und die erste Interaktivität erforderlich sind.
  • Rendern Sie Ihre App vor, um die wahrgenommene Ladeleistung zu verbessern.
  • Nutzen Sie die aggressive Codeaufteilung, um die für die Interaktivität erforderliche Codemenge zu reduzieren.

Seien Sie gespannt auf Teil 2, in dem wir diskutieren, wie Sie die Laufzeitleistung auf Geräten mit eingeschränkten Bedingungen optimieren können.