Wenn in der Webentwicklung die Nachfrage steigt und die Codebasis erweitert wird, werden auch die Webseiten, die wir schließlich veröffentlichen, nach und nach erweitert. Allerdings bedeutet diese Erweiterung nicht nur, dass mehr Übertragungsbandbreite belegt wird, sondern auch, dass Benutzer beim Surfen im Internet möglicherweise ein schlechteres Leistungserlebnis haben. Nachdem der Browser die Skripte heruntergeladen hat, von denen eine bestimmte Seite abhängt, muss er noch eine Syntaxanalyse, Interpretation und Ausführung durchführen. In diesem Artikel wird die Verarbeitung von JavaScript durch den Browser eingehend analysiert, die Übeltäter ermittelt, die sich auf die Startzeit Ihrer Anwendung auswirken, und auf der Grundlage meiner persönlichen Erfahrung entsprechende Lösungen vorgeschlagen. Rückblickend haben wir nicht speziell darüber nachgedacht, wie wir die JavaScript-Parsing-/Kompilierungsschritte optimieren können. Wir haben erwartet, dass der Parser den Parsing-Vorgang sofort nach der Entdeckung des <script>
-Tags abschließen würde, aber das ist offensichtlich ein Wunschtraum. Die folgende Abbildung gibt einen Überblick über das Funktionsprinzip des V8-Motors:
Lassen Sie uns die wichtigsten Schritte im Detail analysieren.
Während der Startphase nehmen Syntaxanalyse, Kompilierung und Skriptausführung die meiste Zeit in Anspruch, die die JavaScript-Engine ausführt. Mit anderen Worten: Die durch diese Prozesse verursachte Verzögerung spiegelt tatsächlich die Interaktionsverzögerung des Benutzers wider. Beispielsweise hat der Benutzer eine Schaltfläche gesehen, es dauert jedoch einige Sekunden, bis er tatsächlich darauf klicken kann, was sich stark auf die Benutzererfahrung auswirkt.
Das obige Bild ist das Analyseergebnis einer Website, die die integrierten V8 RunTime Call Stats verwendet. Es ist zu beachten, dass die Syntaxanalyse und -kompilierung in Desktop-Browsern dauert nach oben Es dauert immer noch ziemlich lange, aber auf dem mobilen Endgerät dauert es noch länger. Tatsächlich kann der Zeitaufwand für die Syntaxanalyse und -kompilierung auf großen Websites wie Facebook, Wikipedia und Reddit nicht ignoriert werden:
Der rosa Bereich im Bild oben stellt die dar Zeitaufwand für V8 Im Vergleich zur Zeit in Blinks C++ stellen Orange und Gelb den Zeitanteil für die Syntaxanalyse bzw. -kompilierung dar. Sebastian Markbage von Facebook und Rob Wormald von Google posteten ebenfalls auf Twitter, dass die lange Syntax-Parsing-Zeit von JavaScript zu einem nicht zu ignorierenden Problem geworden sei. Letzterer sagte auch, dass dies auch einer der Hauptverbrauchsfaktoren beim Start von Angular sei.
Mit der ankommenden Welle mobiler Endgeräte müssen wir uns einer grausamen Tatsache stellen: Der Parsing- und Kompilierungsprozess desselben Paketkörpers auf dem mobilen Endgerät kostet genauso viel wie auf Der Desktop-Browser benötigt 2 bis 5 Mal länger. Natürlich bieten High-End-Telefone wie das iPhone oder Pixel eine viel bessere Leistung als Mittelklasse-Telefone wie das Moto G4. Dies erinnert uns daran, dass wir beim Testen nicht nur die High-End-Telefone um uns herum verwenden sollten, sondern auch beide Mittelklasse-Telefone berücksichtigen sollten. Reichweite und Low-End-Telefone:
Das obige Bild ist ein Vergleich der Analysezeit eines 1-MB-JavaScript-Paketkörpers zwischen einigen Desktop-Browsern und mobilen Browsern. Das ist offensichtlich Es gibt große Unterschiede zwischen Mobiltelefonen mit unterschiedlichen Konfigurationen. Wenn der Hauptteil unseres Anwendungspakets bereits sehr groß ist, hat die Verwendung einiger moderner Verpackungstechniken wie Codeaufteilung, TreeShaking, Service Workder-Caching usw. einen großen Einfluss auf die Startzeit. Aus einer anderen Perspektive: Auch wenn es sich um ein kleines Modul handelt, wenn Ihr Code schlecht geschrieben ist oder Sie schlechte Abhängigkeitsbibliotheken verwenden, wird Ihr Hauptthread viel Zeit mit der Kompilierung oder redundanten Funktionsaufrufen verbringen. Wir müssen uns darüber im Klaren sein, wie wichtig eine umfassende Bewertung ist, um die tatsächlichen Leistungsengpässe herauszufinden.
Ich habe jemanden mehr als einmal sagen hören: „Ich bin nicht Facebook“ Welche Auswirkungen wird die von Ihnen erwähnte JavaScript-Syntaxanalyse und -kompilierung
auf andere Websites haben? Ich war auch neugierig auf dieses Thema und habe zwei Monate damit verbracht, mehr als 6.000 Websites zu analysieren, die beliebte Frameworks oder Bibliotheken wie React, Angular, Ember und Vue enthielten. Die meisten Tests basieren auf WebPageTest, sodass Sie diese Testergebnisse problemlos reproduzieren können. Desktop-Browser mit Glasfaserzugang benötigen etwa 8 Sekunden, um eine Benutzerinteraktion zu ermöglichen, während das Moto G4 in einer 3G-Umgebung etwa 16 Sekunden benötigt, um eine Benutzerinteraktion zu ermöglichen.
Die meisten Anwendungen verbringen etwa 4 Sekunden in der JavaScript-Startphase (Grammatikanalyse, Kompilierung, Ausführung) in Desktop-Browsern:
In mobilen Browsern dauert das Parsen der Syntax etwa 36 % länger:
Außerdem zeigen Statistiken dies Nicht alle Websites werfen den Benutzern ein riesiges JS-Paket zu. Die durchschnittliche Größe des von Benutzern heruntergeladenen Gzip-komprimierten Pakets beträgt 410 KB, was im Wesentlichen mit den zuvor von HTTPArchive veröffentlichten 420 KB-Daten übereinstimmt. Aber die schlechteste Website gibt 10 MB Skript direkt an den Benutzer weiter, was einfach schrecklich ist.
Anhand der obigen Statistiken können wir feststellen, dass das Paketvolumen wichtig ist, aber nicht der einzige Faktor ist. Der Zeitaufwand für die Syntaxanalyse und -kompilierung nimmt nicht unbedingt zu mit dem Paketvolumenwachstum und dem linearen Wachstum. Im Allgemeinen wird ein kleines JavaScript-Paket schneller geladen (ohne Unterschiede bei Browsern, Geräten und Netzwerkverbindungen), aber bei gleicher Größe von 200 KB ist die Syntaxanalyse und Kompilierungszeit der Pakete verschiedener Entwickler enorm zueinander und können nicht verglichen werden.
Zeitleiste öffnen (Leistungsbereich) > Bottom-Up/Aufrufbaum/Ereignisprotokoll zeigt das aktuelle an Anteil der Zeit, die die Website für die Syntaxanalyse/-kompilierung aufwendet. Wenn Sie umfassendere Informationen wünschen, können Sie die Runtime Call Stats von V8 aktivieren. In Canary ist es in der Timeline unter „Experims > V8 Runtime Call Stats“ zu finden.
Öffnen Sie die Seite „about:tracing“. Das zugrunde liegende Tracking-Tool von Chrome ermöglicht es uns, disabled-by-default-v8.runtime_stats
zu verwenden, um den Zeitverbrauch genau zu verstehen von V8. V8 bietet außerdem detaillierte Anleitungen zur Verwendung dieser Funktion.
Die Seite „Verarbeitungsaufschlüsselung“ in WebPageTest wird automatisch aufgezeichnet, wenn wir Chrome > aktivieren Timeline V8-Kompilierungs-, EvaluateScript- und FunctionCall-Zeiten. Wir können auch Runtime Call Stats aktivieren, indem wir disabled-by-default-v8.runtime_stats
angeben.
Weitere Anweisungen finden Sie in meinem Kern.
Wir können auch die von Nolan Lawson empfohlene Benutzer-Timing-API verwenden, um die Zeit der Grammatikanalyse zu bewerten. Diese Methode kann jedoch durch den V8-Voranalyseprozess beeinträchtigt werden. Wir können aus Nolans Methode in der Optimize-JS-Auswertung lernen und am Ende des Skripts eine zufällige Zeichenfolge hinzufügen, um dieses Problem zu lösen. Ich verwende eine ähnliche Methode, die auf Google Analytics basiert, um die Analysezeit auszuwerten, wenn echte Benutzer und Geräte die Website besuchen:
Das DeviceTiming-Tool von Etsy kann simulieren Bewerten Sie die Syntaxanalyse und Ausführungszeit der Seite in einigen eingeschränkten Umgebungen. Es verpackt lokale Skripte in einen Instrumentierungstoolcode, sodass unsere Seite den Zugriff von verschiedenen Geräten simulieren kann. Weitere Informationen zur Verwendung finden Sie im Artikel „Benchmarking JS Parsing and Execution on Mobile Devices“ von Daniel Espeset.
Reduzieren Sie die Körpergröße des JavaScript-Pakets. Wir haben oben auch erwähnt, dass kleinere Paketkörper oft einen geringeren Arbeitsaufwand beim Parsen bedeuten, was auch den Zeitverbrauch des Browsers in den Phasen des Parsens und Kompilierens reduzieren kann.
Verwenden Sie Code-Splitting-Tools, um Code bei Bedarf weiterzugeben und verbleibende Module verzögert zu laden. Dies ist wahrscheinlich der beste Ansatz, da Modelle wie PRPL die routenbasierte Gruppierung fördern und derzeit häufig von Flipkart, Housing.com und Twitter verwendet werden.
Skript-Streaming: In der Vergangenheit ermutigte V8 Entwickler, async/defer
zu verwenden, um eine Leistungsverbesserung von 10–20 % basierend auf Skript-Streaming zu erreichen. Diese Technik ermöglicht es dem HTML-Parser, entsprechende Skriptladeaufgaben einem dedizierten Skript-Streaming-Thread zuzuweisen und so eine Blockierung der Dokumentenanalyse zu vermeiden. V8 empfiehlt, größere Module so früh wie möglich zu laden, schließlich haben wir nur einen Streamer-Thread.
Bewerten Sie die Kosten für das Parsen unserer Abhängigkeiten. Wir sollten unser Bestes geben, um Abhängigkeiten auszuwählen, die die gleiche Funktionalität haben, aber schneller geladen werden, wie zum Beispiel die Verwendung von Preact oder Inferno anstelle von React, die kleiner sind und weniger Syntaxanalyse und Kompilierungszeit als React erfordern. Paul Lewis erörterte auch die Kosten für den Start eines Frameworks in einem kürzlich erschienenen Artikel, der mit der Aussage von Sebastian Markbage übereinstimmt: „Der beste Weg, die Startkosten eines Frameworks zu bewerten, besteht darin, zuerst eine Schnittstelle zu rendern, sie dann zu löschen und schließlich neu zu rendern.“ Der erste Rendering-Prozess umfasst die Analyse und Kompilierung. Durch Vergleich kann der Startverbrauch des Frameworks ermittelt werden.
Wenn Ihr JavaScript-Framework den AOT-Kompilierungsmodus (Ahead-of-Time) unterstützt, kann dies die Zeit für das Parsen und Kompilieren effektiv verkürzen. Angular-Anwendungen profitieren von diesem Muster:
Lassen Sie sich nicht entmutigen, Sie sind nicht der Einzige, der mit der Verbesserung der Startzeit zu kämpfen hat. Auch unser V8-Team hat hart gearbeitet. Wir haben festgestellt, dass Octane, ein früheres Evaluierungstool, eine gute Simulation realer Szenarien darstellt. Es stimmt in Bezug auf Mikroframework und Kaltstart sehr gut mit den tatsächlichen Benutzergewohnheiten überein. Basierend auf diesen Tools hat das V8-Team in früheren Arbeiten auch eine Verbesserung der Startleistung um etwa 25 % erreicht:
In diesem Abschnitt werden wir die Tools überprüfen, die wir in verwendet haben Es werden Techniken zur Verbesserung der Syntaxanalyse und der Kompilierungszeit erläutert.
Chrome 42 begann mit der Einführung des Konzepts des sogenannten Code-Cache, der uns einen Mechanismus zum Speichern kompilierter Codekopien bietet Wenn der Benutzer die Seite zum zweiten Mal besucht, können auf diese Weise die Schritte des Skript-Crawlings, Parsens und Kompilierens vermieden werden. Darüber hinaus haben wir festgestellt, dass dieser Mechanismus auch bei wiederholtem Zugriff etwa 40 % der Kompilierungszeit einsparen kann. Hier werde ich einige Inhalte ausführlich vorstellen:
Code-Caching wird durchgeführt wiederholt innerhalb von 72 Arbeitsstunden ausgeführt.
Bei Skripten in Service Worker funktioniert das Code-Caching auch für Skripte innerhalb von 72 Stunden.
Bei Skripten, die mithilfe eines Service Workers im Cache-Speicher zwischengespeichert werden, funktioniert das Code-Caching bei der ersten Ausführung des Skripts.
Kurz gesagt: Bei aktiv zwischengespeichertem JavaScript-Code können die Syntaxanalyse- und Kompilierungsschritte höchstens beim dritten Aufruf übersprungen werden. Wir können chrome://flags/#v8-cache-strategies-for-cache-storage
verwenden, um den Unterschied zu sehen, oder wir können js-flags=profile-deserialization
so einstellen, dass Chrome ausgeführt wird, um zu sehen, ob der Code aus dem Code-Cache geladen wird. Es ist jedoch zu beachten, dass der Code-Caching-Mechanismus nur kompilierten Code zwischenspeichert, der sich hauptsächlich auf den Code der obersten Ebene bezieht, der häufig zum Festlegen globaler Variablen verwendet wird. Lazy kompilierter Code wie Funktionsdefinitionen wird nicht zwischengespeichert, aber IIFE ist auch in V8 enthalten, sodass diese Funktionen ebenfalls zwischengespeichert werden können.
Skript-Streaming ermöglicht das Parsen asynchroner Skripte in einem Hintergrundthread, wodurch die Seitenladezeit um etwa 10 % verkürzt werden kann. Wie oben erwähnt, funktioniert dieser Mechanismus auch für Synchronisationsskripte.
Diese Funktion wird zum ersten Mal erwähnt, sodass V8 alle Skripte zulässt, sogar blockierende <script src=''>
Skripte können von Hintergrundthreads analysiert werden. Der Nachteil besteht jedoch darin, dass es derzeit nur einen Streaming-Hintergrundthread gibt. Wir empfehlen daher, zunächst große, kritische Skripte zu analysieren. In der Praxis empfehlen wir, <script defer>
innerhalb des <head>
-Blocks hinzuzufügen, damit die Browser-Engine das zu analysierende Skript so früh wie möglich erkennen und es dann einem Hintergrundthread zur Verarbeitung zuweisen kann. Wir können auch die DevTools-Zeitleiste überprüfen, um festzustellen, ob das Skript im Hintergrund analysiert wird. Insbesondere wenn Sie ein kritisches Skript haben, das analysiert werden muss, müssen Sie sicherstellen, dass das Skript vom Streaming-Thread analysiert wird.
Wir sind außerdem bestrebt, einen leichteren und schnelleren Parser zu entwickeln, der derzeit den größten Engpass im V8-Hauptthread darstellt liegt im sogenannten nichtlinearen analytischen Verbrauch. Wir haben zum Beispiel den folgenden Codeteil:
(function (global, module) { … })(this, function module() { my functions })
V8 weiß beim Kompilieren des Hauptskripts nicht, ob wir das Modul module
benötigen, daher geben wir die Kompilierung vorübergehend auf. Und wenn wir planen, module
zu kompilieren, müssen wir alle internen Funktionen erneut analysieren. Dies ist der Grund für die sogenannte Nichtlinearität der V8-Analysezeit. Jede Funktion mit einer Tiefe von N kann N-mal erneut analysiert werden. V8 ist bereits beim ersten Kompilieren in der Lage, Informationen über alle internen Funktionen zu sammeln, sodass V8 bei zukünftigen Kompilierungen alle internen Funktionen ignorieren wird. Für die obige Funktion in der Form module
stellt dies eine große Leistungsverbesserung dar. Für weitere Informationen wird empfohlen, „The V8 Parser(s) – Design, Challenges, and Parsing JavaScript Better“ zu lesen. V8 sucht außerdem nach einem geeigneten Offloading-Mechanismus, um sicherzustellen, dass der JavaScript-Kompilierungsprozess beim Start in einem Hintergrundthread ausgeführt werden kann.
Alle paar Jahre schlägt jemand vor, dass Engines einen Mechanismus zur Verarbeitung vorkompilierter Skripte bereitstellen sollten. Mit anderen Worten: Entwickler können Build-Tools oder andere serverseitige Tools verwenden, um Skripte in Bytecode umzuwandeln und sie dann direkt auszuführen Der Browser Diese Bytecodes reichen aus. Aus meiner persönlichen Sicht bedeutet die direkte Übertragung von Bytecode einen größeren Paketkörper, was zwangsläufig die Ladezeit verlängert, und wir müssen den Code signieren, um sicherzustellen, dass er sicher ausgeführt werden kann. Unsere aktuelle Positionierung für V8 besteht darin, die oben erwähnte interne Neuanalyse so weit wie möglich zu vermeiden, um die Startzeit zu verbessern, während die Vorkompilierung zusätzliche Risiken mit sich bringt. Wir laden jedoch alle ein, dieses Thema gemeinsam zu diskutieren, obwohl sich V8 derzeit auf die Verbesserung der Kompilierungseffizienz und die Förderung der Verwendung von Service Worker-Cache-Skriptcode zur Verbesserung der Starteffizienz konzentriert. Wir haben bei BlinkOn7 auch mit Facebook und Akamai über die Vorkompilierung gesprochen.
JavaScript-Engines wie V8 analysieren die meisten Funktionen im Skript vorab, bevor sie die vollständige Analyse durchführen. Dies liegt hauptsächlich daran, dass die meisten Seiten keine JavaScript-Funktion enthalten sofort ausgeführt.
Vorkompilierung kann die Startzeit verkürzen, indem nur die Mindestfunktionen verarbeitet werden, die der Browser zur Ausführung benötigt. Dieser Mechanismus verringert jedoch tatsächlich die Effizienz im Vergleich zu IIFE. Obwohl die Engine hofft, die Vorverarbeitung dieser Funktionen zu vermeiden, ist sie weitaus weniger effektiv als Bibliotheken wie „optimize-js“. optimieren-js verarbeitet das Skript vor der Engine und fügt Klammern für Funktionen ein, die sofort ausgeführt werden, um eine schnellere Ausführung zu gewährleisten. Diese Art der Vorverarbeitung hat einen sehr guten Optimierungseffekt auf den generierten Paketkörper von Browserify und Webpack, der eine große Anzahl kleiner Module enthält, die sofort ausgeführt werden können. Obwohl dieser kleine Trick nicht das ist, was V8 nutzen möchte, muss der entsprechende Optimierungsmechanismus zum jetzigen Zeitpunkt eingeführt werden.
Die Leistung in der Startphase ist entscheidend. Langsame Parsing-, Kompilierungs- und Ausführungszeiten können zum Engpass für die Leistung Ihrer Webseite werden. Wir sollten die Zeit bewerten, die die Seite in dieser Phase verbringt, und die geeignete Methode zur Optimierung auswählen. Auch an der Verbesserung des Startverhaltens des V8 werden wir weiterhin nach besten Kräften arbeiten!
Das Obige ist der Inhalt der Analyse und Lösung von Leistungsengpässen bei JavaScript-Startups. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn).