Heutige Webanwendungen können ziemlich groß werden, insbesondere der JavaScript-Teil. Seit Mitte 2018 lag die durchschnittliche Übertragungsgröße von JavaScript auf Mobilgeräten laut HTTP-Archiv bei etwa 350 KB. Und das ist nur die Übertragungsgröße! JavaScript wird häufig komprimiert, wenn es über das Netzwerk gesendet wird. Das bedeutet, dass die tatsächliche Menge von JavaScript nach der Dekomprimierung durch den Browser ziemlich viel größer ist. Das ist wichtig, da die Komprimierung für die Ressourcenverarbeitung irrelevant ist. 900 KB dekomprimierten JavaScript sind für den Parser und Compiler immer noch 900 KB, auch wenn es bei der Komprimierung ungefähr 300 KB groß sein kann.
Die Verarbeitung von JavaScript ist eine teure Ressource. Im Gegensatz zu Bildern, bei denen nach dem Download nur eine relativ einfache Decodierungszeit erforderlich ist, muss JavaScript geparst, kompiliert und schließlich ausgeführt werden. Byte für Byte, wodurch JavaScript teurer als andere Ressourcentypen wird.
Während ständig Verbesserungen vorgenommen werden, um die Effizienz von JavaScript-Engines zu verbessern, ist die Verbesserung der JavaScript-Leistung wie immer eine Aufgabe für Entwickler.
Zu diesem Zweck gibt es Techniken zur Verbesserung der JavaScript-Leistung. Die Codeaufteilung ist eine solche Technik, mit der die Leistung verbessert wird, indem Anwendungs-JavaScript in Blöcke partitioniert wird und diese Blöcke nur an die Routen einer Anwendung bereitgestellt werden, die sie benötigen.
Diese Technik funktioniert zwar, lässt sich jedoch nicht an ein häufiges Problem bei Anwendungen mit hoher JavaScript-Nutzung, nämlich der Einbeziehung von Code, der nie verwendet wird, angehen. Das Baumschütteln versucht, dieses Problem zu lösen.
Was versteht man unter Baumschütteln?
Baumschütteln ist eine Form der Beseitigung von totem Code. Der Begriff wurde durch Rollup bekannt gemacht, aber das Konzept der Eliminierung von totem Code existiert schon seit einiger Zeit. Für das Konzept wurden auch Käufe in Webpack gefunden, was in diesem Artikel anhand einer Beispiel-App demonstriert wird.
Der Begriff „Baumwackeln“ stammt vom mentalen Modell Ihrer Anwendung und ihrer Abhängigkeiten in Form einer baumähnlichen Struktur. Jeder Knoten in der Baumstruktur stellt eine Abhängigkeit dar, die unterschiedliche Funktionen für Ihre Anwendung bietet. In modernen Anwendungen werden diese Abhängigkeiten über statische import
-Anweisungen wie folgt eingebunden:
// Import all the array utilities!
import arrayUtils from "array-utils";
Wenn eine App jung ist – wenn es sich um Junge handelt – kann es sein, dass sie nur wenige Abhängigkeiten hat. Dabei werden auch die meisten – wenn nicht alle – die Abhängigkeiten verwendet, die Sie hinzufügen. Mit zunehmender Reife Ihrer App können jedoch weitere Abhängigkeiten hinzugefügt werden. Außerdem werden ältere Abhängigkeiten nicht mehr verwendet, aber möglicherweise nicht aus Ihrer Codebasis entfernt. Das Endergebnis ist, dass eine App mit viel nicht verwendetem JavaScript ausgeliefert wird. Das Baumschütteln bekämpft dieses Problem, indem es nutzt, wie statische import
-Anweisungen bestimmte Teile von ES6-Modulen abrufen:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
Der Unterschied zwischen diesem import
- und dem vorherigen Beispiel besteht darin, dass in diesem Beispiel nicht alles aus dem "array-utils"
-Modul importiert wird – was sehr viel Code sein könnte –, sondern nur bestimmte Teile davon importiert werden. Bei Entwicklungs-Builds ändert sich nichts, da das gesamte Modul trotzdem importiert wird. In Produktions-Builds kann das Webpack so konfiguriert werden, dass Exporte aus ES6-Modulen, die nicht explizit importiert wurden, „abgeschüttelt“ werden. Dadurch werden diese Produktions-Builds kleiner. In diesem Leitfaden erfahren Sie, wie das geht.
Möglichkeiten finden, einen Baum zu schütteln
Zur Veranschaulichung ist eine einseitige Beispielanwendung verfügbar, die die Funktionsweise des Tree Shakings demonstriert. Sie können ihn klonen und der Anleitung folgen, wenn Sie möchten. In diesem Leitfaden werden wir jeden Schritt des Weges gemeinsam behandeln. Klonen ist also nicht erforderlich, es sei denn, praxisorientiertes Lernen ist Ihre Aufgabe.
Die Beispiel-App ist eine durchsuchbare Datenbank mit Gitarreneffektpedalen. Wenn Sie eine Abfrage eingeben, wird eine Liste der Effektpedale angezeigt.
Das Verhalten dieser Anwendung wird nach Anbieter getrennt (d.h. Preact und Emotion) sowie appspezifische Code-Bundles (oder „Chunks“, wie sie von Webpack bezeichnet werden):
Die in der Abbildung oben gezeigten JavaScript-Bundles sind Produktions-Builds, d. h. sie werden durch Uglification optimiert. 21,1 KB für ein app-spezifisches Bundle ist nicht schlecht, aber angemerkt, dass kein Tree Shaking auftritt. Sehen wir uns den App-Code an und überlegen Sie, wie Sie dieses Problem beheben können.
Zur Ermittlung von Möglichkeiten zur Baumschüttelung muss in jeder Anwendung nach statischen import
-Anweisungen gesucht werden. Am oberen Rand der Hauptkomponentendatei sehen Sie eine Zeile, die wie folgt aussieht:
import * as utils from "../../utils/utils";
Sie können ES6-Module auf verschiedene Weise importieren, aber solche sollten Sie Aufmerksamkeit erregen. In dieser Zeile steht: „import
alles aus dem utils
-Modul in einem Namespace namens utils
.“ Die wichtige Frage an dieser Stelle lautet: „Wie viel Inhalte sind in diesem Modul?“
Im Quellcode des Moduls utils
sehen Sie etwa 1.300 Codezeilen.
Brauchst du all diese Dinge? Prüfen wir das noch einmal. Suchen Sie dazu in der Hauptkomponentendatei, die das utils
-Modul importiert, um zu sehen, wie viele Instanzen dieses Namespace angezeigt werden.
Wie sich herausstellt, erscheint der utils
-Namespace in der Anwendung nur an drei Stellen – aber für welche Funktionen? Wenn Sie sich die Hauptkomponentendatei noch einmal ansehen, scheint es nur eine Funktion zu sein, nämlich utils.simpleSort
. Damit wird die Liste der Suchergebnisse nach einer Reihe von Kriterien sortiert, wenn die Drop-down-Menüs zum Sortieren geändert werden:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
Aus einer Datei mit 1.300 Zeilen und einer Reihe von Exporten wird nur einer davon verwendet. Dies führt dazu, dass viel nicht verwendetes JavaScript versendet wird.
Diese Beispiel-App ist zwar zugegebenermaßen etwas durchdacht, ändert aber nichts an der Tatsache, dass dieses synthetische Szenario den tatsächlichen Optimierungsmöglichkeiten ähnelt, die Sie in einer Produktions-Webanwendung haben könnten. Sie haben nun eine Möglichkeit identifiziert, die Baumwolken nutzen kann. Wie funktioniert das eigentlich?
Es wird verhindert, dass Babel ES6-Module in CommonJS-Module transpiliert
Babel ist ein unverzichtbares Werkzeug, kann die Auswirkungen von Baumwackeln aber etwas erschweren. Wenn Sie @babel/preset-env
verwenden, kann Babel ES6-Module in weiter kompatible CommonJS-Module umwandeln, d. h. Module, die require
statt import
.
Da Baumwackeln bei CommonJS-Modulen schwieriger auszuführen ist, weiß Webpack nicht, was aus Bundles entfernt werden soll, wenn Sie sie verwenden. Die Lösung besteht darin, @babel/preset-env
so zu konfigurieren, dass ES6-Module explizit ignoriert werden. Unabhängig von der Konfiguration von Babel, sei es in babel.config.js
oder package.json
, müssen Sie etwas zusätzliches hinzufügen:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Wenn Sie modules: false
in Ihrer @babel/preset-env
-Konfiguration angeben, verhält sich Babel wie gewünscht, wodurch Webpack Ihren Abhängigkeitsbaum analysieren und nicht verwendete Abhängigkeiten abschütteln kann.
Nebenwirkungen berücksichtigen
Ein weiterer Aspekt, den Sie beim Schütteln von Abhängigkeiten von Ihrer Anwendung berücksichtigen sollten, ist, ob die Module Ihres Projekts Nebenwirkungen haben. Ein Beispiel für einen Nebeneffekt ist, wenn eine Funktion etwas ändert, das außerhalb ihres eigenen Geltungsbereichs liegt, was ein Nebeneffekt der Ausführung ist:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
In diesem Beispiel führt addFruit
zu einem Nebeneffekt, wenn das fruits
-Array geändert wird, das außerhalb des Geltungsbereichs liegt.
Nebenwirkungen gelten auch für ES6-Module, und das ist im Zusammenhang mit Tree Shakings von Bedeutung. Module, die vorhersehbare Eingaben nutzen und gleichermaßen vorhersehbare Ausgaben erzeugen, ohne etwas außerhalb ihres eigenen Bereichs zu ändern, sind Abhängigkeiten, die sicher gelöscht werden können, wenn wir sie nicht verwenden. Es handelt sich um eigenständiges, modulares Code-Snippet. Daher „Module“.
Bei Webpack kann ein Hinweis verwendet werden, um anzugeben, dass ein Paket und seine Abhängigkeiten frei von Nebeneffekten sind. Dazu gibst du "sideEffects": false
in der package.json
-Datei eines Projekts an:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
Alternativ kannst du Webpack mitteilen, welche Dateien keine Nebeneffekte haben:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
Im letzteren Beispiel wird angenommen, dass jede nicht angegebene Datei frei von Nebeneffekten ist. Wenn du dies nicht deiner package.json
-Datei hinzufügen möchtest, kannst du es auch über module.rules
in der Webpack-Konfiguration angeben.
Nur benötigte Elemente importieren
Nachdem Sie Babel angewiesen haben, die ES6-Module außer Acht zu lassen, ist eine leichte Anpassung der import
-Syntax erforderlich, um nur die Funktionen einzufügen, die aus dem utils
-Modul notwendig sind. In diesem Beispiel wird lediglich die Funktion simpleSort
benötigt:
import { simpleSort } from "../../utils/utils";
Da nur simpleSort
und nicht das gesamte utils
-Modul importiert wird, muss jede Instanz von utils.simpleSort
zu simpleSort
geändert werden:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
Das sollte alles sein, was erforderlich ist, damit die Baumwacklerfunktion in diesem Beispiel funktioniert. So sieht die Webpack-Ausgabe aus, bevor der Abhängigkeitsbaum geschüttelt wird:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
So sieht die Ausgabe nach erfolgreichem Baumschütteln aus:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
Beide Sets haben zwar schrumpfend, aber es ist wirklich das main
-Bundle, das am meisten davon profitiert. Durch das Abschütteln der nicht verwendeten Teile des utils
-Moduls schrumpft das main
-Bundle um etwa 60%. Dadurch verkürzt sich nicht nur die Zeit bis zum Download, sondern auch die Verarbeitungszeit des Skripts.
Schüttle ein paar Bäume!
Was auch immer Sie beim Baumschütteln bewältigt, hängt von Ihrer App und ihren Abhängigkeiten und ihrer Architektur ab. Jetzt testen Wenn Sie Ihren Modul-Bundler nicht für diese Optimierung eingerichtet haben, schadet der Versuch nicht, die Vorteile für Ihre Anwendung zu erkennen.
Das Baumschütteln kann einen deutlichen Leistungszuwachs bedeuten oder Sie können gar nicht so viel erreichen. Wenn Sie Ihr Build-System jedoch so konfigurieren, dass diese Optimierung in Produktions-Builds genutzt wird, und nur die Daten importieren, die Ihre Anwendung benötigt, halten Sie Ihre App Bundles proaktiv so klein wie möglich.
Vielen Dank an Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone und Philip Walton für ihr wertvolles Feedback, durch das die Qualität dieses Artikels deutlich verbessert wurde.