Modernen Code in modernen Browsern bereitstellen, um die Ladezeiten zu verkürzen

Houssein Dschirdeh
Houssein Djirdeh

Verbessern Sie in diesem Codelab die Leistung dieser einfachen Anwendung, mit der Nutzer zufällig Katzen bewerten können. Hier erfahren Sie, wie Sie das JavaScript-Bundle optimieren, indem Sie die Menge der zu transpilierenden Codes minimieren.

App – Screenshot

In der Beispiel-App können Sie ein Wort oder ein Emoji auswählen, um zu vermitteln, wie sehr Ihnen jede Katze gefällt. Wenn Sie auf eine Schaltfläche klicken, zeigt die App den Wert der Schaltfläche unter dem aktuellen Katzenbild an.

Messen

Es empfiehlt sich immer, zuerst eine Website zu prüfen, bevor Sie Optimierungen vornehmen:

  1. Wenn Sie eine Vorschau der Website aufrufen möchten, klicken Sie auf View App (App anzeigen) und dann auf Fullscreen (Vollbild) Vollbild.
  2. Drücken Sie „Strg + Umschalttaste + J“ (oder „Befehlstaste + Option + J“ auf dem Mac), um die Entwicklertools zu öffnen.
  3. Klicken Sie auf den Tab Netzwerk.
  4. Klicken Sie das Kästchen Cache deaktivieren an.
  5. Aktualisieren Sie die App.

Anfrage für ursprüngliche Paketgröße

Für diese Anwendung werden mehr als 80 KB verwendet. So finden Sie heraus, ob Teile des Bundles nicht verwendet werden:

  1. Drücken Sie Control+Shift+P (oder Command+Shift+P auf einem Mac), um das Menü Befehl zu öffnen. Befehlsmenü

  2. Geben Sie Show Coverage ein und drücken Sie Enter, um den Tab Abdeckung aufzurufen.

  3. Klicken Sie auf dem Tab Abdeckung auf Aktualisieren, um die Anwendung während der Erfassung der Abdeckung neu zu laden.

    App mit Codeabdeckung aktualisieren

  4. Sehen Sie sich an, wie viel Code verwendet und wie viel Code für das Haupt-Bundle geladen wurde:

    Codeabdeckung des Sets

Mehr als die Hälfte des Pakets (44 KB) wird gar nicht genutzt. Das liegt daran, dass ein großer Teil des darin enthaltenen Codes aus Polyfills besteht, damit die Anwendung auch in älteren Browsern funktioniert.

@babel/preset-env verwenden

Die Syntax der JavaScript-Sprache entspricht dem Standard ECMAScript oder ECMA-262. Jedes Jahr werden neuere Versionen der Spezifikation veröffentlicht, die neue Funktionen enthalten, die den Angebotsprozess bestanden haben. Jeder Browser befindet sich in einer anderen Phase der Unterstützung dieser Funktionen.

Die folgenden ES2015-Funktionen werden in der Anwendung verwendet:

Die folgende ES2017-Funktion wird ebenfalls verwendet:

Sie können sich den Quellcode in src/index.js ansehen, um zu sehen, wie er verwendet wird.

Alle diese Funktionen werden in der neuesten Version von Chrome unterstützt. Aber was ist mit anderen Browsern, die sie nicht unterstützen? Die in der Anwendung enthaltene Babel-Bibliothek ist die gängigste Bibliothek, die zum Kompilieren von Code mit neuerer Syntax in Code verwendet wird, den ältere Browser und Umgebungen verstehen können. Dies erfolgt auf zwei Arten:

  • Polyfills werden verwendet, um neuere Funktionen von ES2015+ zu emulieren, sodass ihre APIs auch dann verwendet werden können, wenn sie vom Browser nicht unterstützt werden. Hier ist ein Beispiel für einen polyfill für die Methode Array.includes.
  • Plug-ins werden verwendet, um ES2015-Code (oder höher) in ältere ES5-Syntax umzuwandeln. Da es sich um Syntaxänderungen (wie Pfeilfunktionen) handelt, können sie nicht mit Polyfills emuliert werden.

In package.json sehen Sie, welche Babel-Bibliotheken enthalten sind:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core ist der zentrale Babel-Compiler. Damit werden alle Babel-Konfigurationen in einem .babelrc im Stammverzeichnis des Projekts definiert.
  • babel-loader enthält Babel in den Webpack-Build-Prozess.

Unter webpack.config.js sehen Sie, wie babel-loader als Regel verwendet wird:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill stellt alle erforderlichen Polyfills für alle neueren ECMAScript-Funktionen bereit, damit sie in Umgebungen funktionieren, die sie nicht unterstützen. Es wurde bereits ganz oben in src/index.js. importiert
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env gibt an, welche Transformationen und Polyfills für Browser oder Umgebungen erforderlich sind, die als Ziele ausgewählt werden.

Informationen zum Inhalt der Babel-Konfigurationsdatei .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Dies ist eine Babel- und Webpack-Einrichtung. Hier erfahren Sie, wie Sie Babel in Ihre Anwendung einbinden, falls Sie einen anderen Modul-Bundler als Webpack verwenden.

Das Attribut targets in .babelrc gibt an, welche Browser ausgewählt werden. @babel/preset-env lässt sich in die Browserliste einbinden. Eine vollständige Liste kompatibler Abfragen, die in diesem Feld verwendet werden können, finden Sie in der Browserlist-Dokumentation.

Mit dem Wert "last 2 versions" wird der Code in der Anwendung für die letzten beiden Versionen jedes Browsers transpiliert.

Debugging

Um einen vollständigen Überblick über alle Babel-Ziele des Browsers sowie alle enthaltenen Transformationen und Polyfills zu erhalten, fügen Sie .babelrc: das Feld debug hinzu.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Klicken Sie auf Tools.
  • Klicken Sie auf Logs.

Aktualisieren Sie die Anwendung und sehen Sie sich die Glitch-Statuslogs am unteren Rand des Editors an.

Zielbrowser

Babel protokolliert in der Konsole eine Reihe von Details zum Kompilierungsprozess, einschließlich aller Zielumgebungen, für die der Code kompiliert wurde.

Zielbrowser

Beachten Sie, dass nicht mehr unterstützte Browser wie Internet Explorer in dieser Liste enthalten sind. Dies ist ein Problem, da für nicht unterstützte Browser keine neueren Funktionen hinzugefügt werden und Babel weiterhin die spezifische Syntax für sie transpiliert. Dadurch wird Ihr Bundle unnötig groß, wenn Nutzer diesen Browser nicht für den Zugriff auf Ihre Website verwenden.

Babel protokolliert auch eine Liste der verwendeten Transformations-Plug-ins:

Liste der verwendeten Plug-ins

Das ist eine ziemlich lange Liste. Dies sind alle Plug-ins, die Babel verwenden muss, um jede Syntax aus ES2015+ für alle Zielbrowser in eine ältere Syntax umzuwandeln.

In Babel werden jedoch keine spezifischen Polyfills angezeigt, die verwendet werden:

Keine Polyfills hinzugefügt

Das liegt daran, dass der gesamte @babel/polyfill direkt importiert wird.

Polyfills einzeln laden

Wenn @babel/polyfill in eine Datei importiert wird, schließt Babel standardmäßig alle Polyfills ein, die für eine vollständige ES2015+-Umgebung erforderlich sind. Wenn Sie bestimmte Polyfills importieren möchten, die für die Zielbrowser erforderlich sind, fügen Sie der Konfiguration ein useBuiltIns: 'entry' hinzu.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Aktualisieren Sie die Anwendung. Sie sehen jetzt alle enthaltenen Polyfills:

Liste der importierten Polyfills

Jetzt sind zwar nur noch benötigte Polyfills für "last 2 versions" enthalten, die Liste ist aber immer noch sehr lang. Das liegt daran, dass Polyfills, die für die Zielbrowser für alle neueren Funktionen benötigt werden, weiterhin enthalten sind. Ändern Sie den Wert des Attributs in usage, um nur die Elemente einzuschließen, die für im Code verwendete Elemente erforderlich sind.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Polyfills werden bei Bedarf automatisch eingefügt. Das bedeutet, dass Sie den Import von @babel/polyfill in src/index.js. entfernen können

import "./style.css";
import "@babel/polyfill";

Jetzt sind nur noch die für die Anwendung erforderlichen Polyfills enthalten.

Liste der automatisch eingeschlossenen Polyfills

Die Größe des Anwendungs-Bundles wurde erheblich reduziert.

Paketgröße auf 30,1 KB reduziert

Liste der unterstützten Browser eingrenzen

Die Anzahl der enthaltenen Browserziele ist immer noch recht groß und nicht viele Nutzer verwenden nicht mehr unterstützte Browser wie Internet Explorer. Aktualisieren Sie die Konfigurationen so:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Sehen Sie sich die Details zum abgerufenen Bundle an.

Paketgröße von 30,0 KB

Da die Anwendung so klein ist, gibt es wirklich keinen großen Unterschied bei diesen Änderungen. Es wird jedoch empfohlen, einen Prozentsatz für den Browser-Marktanteil (z. B. ">0.25%") zu verwenden und bestimmte Browser auszuschließen, bei denen Sie sicher sind, dass Ihre Nutzer sie nicht verwenden. Weitere Informationen dazu finden Sie im Artikel Letzte 2 Versionen als schädlich von James Kyle.

<script type="module"> verwenden

Es gibt noch mehr Raum für Verbesserungen. Obwohl einige nicht verwendete Polyfills entfernt wurden, werden derzeit viele davon ausgeliefert, die für manche Browser nicht benötigt werden. Durch die Verwendung von Modulen kann neue Syntax geschrieben und direkt an Browser gesendet werden, ohne dass unnötige Polyfills verwendet werden.

JavaScript-Module sind eine relativ neue Funktion, die in allen gängigen Browsern unterstützt wird. Module können mit einem type="module"-Attribut erstellt werden, um Skripts zu definieren, die aus anderen Modulen importiert und exportiert werden. Beispiel:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

Viele neuere ECMAScript-Funktionen werden bereits in Umgebungen unterstützt, die JavaScript-Module unterstützen (also nicht auf Babel). Das bedeutet, dass die Babel-Konfiguration so geändert werden kann, dass zwei verschiedene Versionen Ihrer Anwendung an den Browser gesendet werden:

  • Eine Version, die in neueren Browsern funktioniert, die Module unterstützen und ein Modul enthält, das größtenteils nicht transpiliert ist, aber eine kleinere Dateigröße hat
  • Eine Version mit einem größeren, transpilierten Skript, das in jedem älteren Browser funktioniert

ES-Module mit Babel verwenden

Wenn Sie separate @babel/preset-env-Einstellungen für die beiden Versionen der Anwendung haben möchten, entfernen Sie die Datei .babelrc. Sie können der Webpack-Konfiguration Babel-Einstellungen hinzufügen, indem Sie für jede Version der Anwendung zwei verschiedene Kompilierungsformate angeben.

Fügen Sie zuerst eine Konfiguration für das alte Skript in webpack.config.js hinzu:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Beachten Sie, dass für "@babel/preset-env" nicht der Wert targets, sondern esmodules mit dem Wert false verwendet wird. Das bedeutet, dass Babel alle erforderlichen Transformationen und Polyfills enthält, die auf jeden Browser ausgerichtet sind, der ES-Module noch nicht unterstützt.

Fügen Sie am Anfang der Datei webpack.config.js die Objekte entry, cssRule und corePlugins hinzu. Diese werden sowohl vom Modul als auch von den Legacy-Skripts, die an den Browser gesendet werden, gemeinsam genutzt.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

Erstellen Sie auf ähnliche Weise ein Konfigurationsobjekt für das folgende Modulskript, in dem legacyConfig definiert ist:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Der Hauptunterschied besteht darin, dass für den Namen der Ausgabedatei die Dateiendung .mjs verwendet wird. Der Wert esmodules ist hier auf "true" gesetzt. Das bedeutet, dass der in dieses Modul ausgegebene Code ein kleineres, weniger kompiliertes Skript ist, das in diesem Beispiel keine Transformation durchläuft, da alle verwendeten Funktionen bereits in Browsern unterstützt werden, die Module unterstützen.

Exportieren Sie am Ende der Datei beide Konfigurationen in ein einziges Array.

module.exports = [
  legacyConfig, moduleConfig
];

Dadurch wird sowohl ein kleineres Modul für Browser erstellt, die es unterstützen, als auch ein größeres transpiliertes Skript für ältere Browser.

Browser, die Module unterstützen, ignorieren Skripts mit einem nomodule-Attribut. Umgekehrt ignorieren Browser, die keine Module unterstützen, Skriptelemente mit type="module". Sie können also sowohl ein Modul als auch ein kompiliertes Fallback einschließen. Idealerweise sollten sich die beiden Versionen der Anwendung in index.html befinden:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

Browser, die Module unterstützen, rufen main.mjs ab und führen sie aus und ignorieren main.bundle.js.. Browser, die keine Module unterstützen, tun das Gegenteil.

Beachten Sie, dass Modulskripts, anders als reguläre Skripts, standardmäßig verzögert werden. Wenn das entsprechende nomodule-Skript ebenfalls zurückgestellt und erst nach dem Parsen ausgeführt werden soll, müssen Sie das Attribut defer hinzufügen:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

Als letzten Schritt müssen Sie dem Modul bzw. dem Legacy-Skript die Attribute module und nomodule hinzufügen. Importieren Sie das ScriptExtHtmlWebpackPlugin ganz oben in webpack.config.js:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

Aktualisieren Sie jetzt das plugins-Array in den Konfigurationen, um dieses Plug-in einzuschließen:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

Mit diesen Plug-in-Einstellungen wird ein type="module"-Attribut für alle .mjs-Skriptelemente sowie ein nomodule-Attribut für alle .js-Skriptmodule hinzugefügt.

Module im HTML-Dokument bereitstellen

Als Letztes müssen Sie sowohl die alten als auch die modernen Skriptelemente in die HTML-Datei ausgeben. Leider unterstützt das Plug-in, mit dem die endgültige HTML-Datei HTMLWebpackPlugin erstellt wird, die Ausgabe sowohl des Moduls als auch der Skripts ohne Modul nicht. Obwohl es Problemumgehungen und separate Plug-ins zur Lösung dieses Problems gibt, z. B. BabelMultiTargetPlugin und HTMLWebpackMultiBuildPlugin, wird im Rahmen dieser Anleitung ein einfacherer Ansatz verwendet, um das Modul-Skriptelement manuell hinzuzufügen.

Fügen Sie am Ende der Datei in src/index.js Folgendes hinzu:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Laden Sie nun die Anwendung in einem Browser, der Module unterstützt, z. B. die neueste Version von Chrome.

5,2-KB-Modul wurde für neuere Browser über das Netzwerk abgerufen

Nur das Modul wird mit einer viel kleineren Bundle-Größe abgerufen, da es größtenteils nicht transpiliert wird. Das andere Skriptelement wird vom Browser vollständig ignoriert.

Wenn Sie die Anwendung in einem älteren Browser laden, wird nur das größere, transpilierte Skript mit allen erforderlichen Polyfills und Transformationen abgerufen. Hier sehen Sie einen Screenshot aller Anfragen, die unter einer älteren Version von Chrome (Version 38) gestellt wurden.

30-KB-Skript für ältere Browser abgerufen

Fazit

Jetzt wissen Sie, wie Sie mit @babel/preset-env nur die erforderlichen Polyfills bereitstellen, die für Zielbrowser erforderlich sind. Außerdem wissen Sie, wie Sie mit JavaScript-Modulen die Leistung weiter verbessern können, indem Sie zwei verschiedene transpilierte Versionen einer Anwendung versenden. Wenn Sie genau wissen, wie diese beiden Techniken die Größe Ihres Pakets erheblich reduzieren können, können Sie weitere Optimierungen vornehmen.