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

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

App – Screenshot

In der Beispiel-App können Sie ein Wort oder ein Emoji auswählen, um anzugeben, wie sehr Sie jede Katze mögen. 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 sich eine Vorschau der Website ansehen möchten, klicken Sie auf App ansehen und dann auf Vollbild Vollbild.
  2. Drücken Sie „Strg + Umschalttaste + J“ (oder „Befehlstaste + Wahltaste + J“ auf einem 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 zur ursprünglichen Paketgröße

Für diese Anwendung werden mehr als 80 KB verwendet. 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 Neu laden, um die Anwendung während der Erfassung der Abdeckung neu zu laden.

    App mit Codeabdeckung aktualisieren

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

    Codeabdeckung des Sets

Mehr als die Hälfte des Bundles (44 KB) wird nicht einmal genutzt. Das liegt daran, dass ein großer Teil des 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 (ECMA-262). Neuere Versionen der Spezifikation werden jedes Jahr veröffentlicht und enthalten neue Funktionen, die das Angebotsverfahren bestanden haben. Jeder große Browser befindet sich immer 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:

Sehen Sie sich den Quellcode von src/index.js an, um zu sehen, wie all dies verwendet wird.

Alle diese Funktionen werden in der neuesten Version von Chrome unterstützt, aber was ist mit anderen Browsern, die diese nicht unterstützen? Das in der Anwendung enthaltene Babel ist die beliebteste Bibliothek zum Kompilieren von Code mit neuerer Syntax in Code, den ältere Browser und Umgebungen verstehen können. Dies geschieht auf zwei Arten:

  • Polyfills sind enthalten, um neuere Funktionen aus 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 der Methode Array.includes.
  • Plug-ins werden verwendet, um ES2015-Code (oder höher) in ältere ES5-Syntax umzuwandeln. Da es sich um syntaxbezogene Änderungen wie Pfeilfunktionen handelt, können sie nicht mit Polyfills emuliert werden.

Unter 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 bezieht Babel beim Erstellen des Webpacks ein.

Sehen Sie sich nun webpack.config.js an, um zu sehen, wie babel-loader als Regel enthalten ist:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill stellt alle erforderlichen Polyfills für neuere ECMAScript-Funktionen bereit, sodass sie in Umgebungen verwendet werden können, 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 alle Browser oder Umgebungen, die als Ziele ausgewählt wurden, erforderlich sind.

In der Babel-Konfigurationsdatei .babelrc sehen Sie, wie sie enthalten ist:

{
  "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, wenn Sie einen anderen Modul-Bundler als Webpack verwenden.

Das Attribut targets in .babelrc gibt an, auf welche Browser die Anzeigen ausgerichtet werden. @babel/preset-env kann in die Browserliste eingebunden werden. Eine vollständige Liste kompatibler Abfragen, die in diesem Feld verwendet werden können, finden Sie in der Dokumentation zur Browserliste.

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

Debugging

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

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

Aktualisiere die Anwendung und sieh dir die Glitch-Statuslogs unten im Editor an.

Ausgewählte Browser

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

Ausgewählte Browser

In dieser Liste sind eingestellte Browser wie Internet Explorer enthalten. Dies ist ein Problem, da für nicht unterstützte Browser keine neueren Funktionen hinzugefügt werden und Babel weiterhin spezifische Syntax für sie transpiliert. Dadurch wird die Größe Ihres Bundles unnötig erhöht, wenn Nutzer nicht über diesen Browser auf Ihre Website zugreifen.

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 ES2015+-Syntax in ältere Syntax für alle Zielbrowser umzuwandeln.

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

Keine Polyfills hinzugefügt

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

Polyfills einzeln laden

Standardmäßig enthält Babel alle Polyfills, die für eine vollständige ES2015+-Umgebung erforderlich sind, wenn @babel/polyfill in eine Datei importiert wird. Wenn Sie bestimmte Polyfills importieren möchten, die für die Zielbrowser erforderlich sind, fügen Sie der Konfiguration 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

Obwohl nur die benötigten Polyfills für "last 2 versions" jetzt enthalten sind, ist die Liste immer noch sehr lang. Das liegt daran, dass Polyfills, die für die Zielbrowser für jede neuere Funktion benötigt werden, weiterhin enthalten sind. Ändern Sie den Wert des Attributs in usage, um nur die Elemente einzubeziehen, die für die im Code verwendeten Elemente erforderlich sind.

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

Dabei werden Polyfills bei Bedarf automatisch eingefügt. Das bedeutet, dass Sie den Import „@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 automatisch eingefügter Polyfills

Die Größe des Anwendungspakets wurde erheblich reduziert.

Bundle-Größe auf 30,1 KB reduziert

Liste der unterstützten Browser eingrenzen

Die Anzahl der enthaltenen Browser ist immer noch recht groß und nur wenige Nutzer verwenden eingestellte 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 bei diesen Änderungen wirklich keinen großen Unterschied. Wir empfehlen Ihnen jedoch, einen Prozentsatz des Browser-Marktanteils (z. B. ">0.25%") zu verwenden und bestimmte Browser auszuschließen, die Ihre Nutzer mit Sicherheit nicht verwenden. Weitere Informationen finden Sie im Artikel Letzte 2 Versionen als schädlich eingestuft von James Kyle.

<script type="module"> verwenden

Es gibt noch mehr Raum für Verbesserungen. Wir haben zwar einige nicht verwendete Polyfills entfernt, werden aber aktuell noch ausgeliefert, werden aber für manche Browser nicht benötigt. Mithilfe von Modulen kann eine neuere 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 in andere Module importiert und daraus 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 (anstelle von Babel). Das bedeutet, dass die Babel-Konfiguration so geändert werden kann, dass zwei verschiedene Versionen Ihrer Anwendung an den Browser gesendet werden:

  • Version, die in neueren Browsern funktioniert, die Module unterstützen und ein Modul enthält, das weitgehend untranspiliert ist, aber eine kleinere Dateigröße hat
  • Eine Version, die ein größeres, transpiliertes Skript enthält, das in jedem älteren Browser funktioniert

ES-Module mit Babel verwenden

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

Fügen Sie webpack.config.js zuerst eine Konfiguration für das alte Skript 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 anstelle des Werts targets für "@babel/preset-env" stattdessen esmodules mit dem Wert false verwendet wird. Das bedeutet, dass Babel alle erforderlichen Transformationen und Polyfills für jeden Browser enthält, der noch keine ES-Module unterstützt.

Fügen Sie die Objekte entry, cssRule und corePlugins am Anfang der Datei webpack.config.js hinzu. Diese werden sowohl vom Modul als auch von Legacy-Skripts, die dem Browser bereitgestellt 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 nun ähnlich ein Konfigurationsobjekt für das Modulskript unten, 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 die Dateiendung .mjs für den Namen der Ausgabedatei verwendet wird. Der Wert esmodules ist hier auf „true“ gesetzt. Das bedeutet, dass der in dieses Modul ausgegebene Code ein kleineres, weniger kompiliertes Script ist, das in diesem Beispiel keine Transformation durchläuft, da alle verwendeten Funktionen bereits in Browsern unterstützt werden, die Module unterstützen.

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

module.exports = [
  legacyConfig, moduleConfig
];

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

In Browsern, die Module unterstützen, werden Skripts mit einem nomodule-Attribut ignoriert. Umgekehrt ignorieren Browser, die keine Module unterstützen, Skriptelemente mit type="module". Das bedeutet, dass Sie sowohl ein Modul als auch ein kompiliertes Fallback einbinden können. Idealerweise sollten sich die beiden Versionen der Anwendung so 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 Module nicht unterstützen, tun das Gegenteil.

Beachten Sie, dass Modulskripts im Gegensatz zu normalen Skripts immer standardmäßig verzögert werden. Wenn Sie möchten, dass auch das entsprechende nomodule-Skript zurückgestellt und erst nach dem Parsen ausgeführt wird, müssen Sie das Attribut defer hinzufügen:

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

Als Letztes 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 nun das Array plugins in den Konfigurationen, um dieses Plug-in einzubeziehen:

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.

Bereitstellungsmodule im HTML-Dokument

Als Letztes müssen Sie sowohl die Legacy- 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 des Moduls und des nomodule-Skripts derzeit nicht. Obwohl es Behelfslösungen und separate Plug-ins gibt, die zur Lösung dieses Problems erstellt wurden, z. B. BabelMultiTargetPlugin und HTMLWebpackMultiBuildPlugin, wird für dieses Tutorial ein einfacherer Ansatz verwendet, bei dem das Modul-Skriptelement manuell hinzugefügt wird.

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

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

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

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

Es wird nur das Modul abgerufen. Die Bundle-Größe ist viel kleiner, da es weitgehend untranspiliert ist. 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 in einer älteren Version von Chrome (Version 38) gestellt wurden.

30 KB-Script für ältere Browser abgerufen

Fazit

Sie wissen jetzt, wie Sie mit @babel/preset-env nur die erforderlichen Polyfills bereitstellen, die für die entsprechenden Browser erforderlich sind. Sie wissen außerdem, wie JavaScript-Module die Leistung weiter verbessern können, wenn zwei verschiedene transpilierte Versionen einer Anwendung ausgeliefert werden. Wenn du weißt, wie du mit diesen beiden Techniken die Paketgröße erheblich reduzieren kannst, kannst du jetzt loslegen und optimieren.