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

In diesem Codelab verbessern Sie die Leistung einer einfachen Anwendung, mit der Nutzer zufällige Katzen bewerten können. Hier erfahren Sie, wie Sie das JavaScript-Bundle optimieren, indem Sie die Menge des transpilierten Codes minimieren.

App – Screenshot

In der Beispiel-App können Sie ein Wort oder Emoji auswählen, um auszudrücken, wie sehr Sie die jeweilige 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, eine Website zu prüfen, bevor Sie Optimierungen vornehmen:

  1. Wenn Sie sich eine Vorschau der Website ansehen möchten, klicken Sie auf View App (App ansehen) und dann auf Fullscreen (Vollbild) Vollbild.
  2. Drücken Sie „Strg + Umschalttaste + J“ (oder „Befehlstaste + Optionstaste + J“ auf einem Mac), um die Entwicklertools zu öffnen.
  3. Klicken Sie auf den Tab Netzwerk.
  4. Setzen Sie ein Häkchen bei Cache deaktivieren.
  5. Aktualisieren Sie die App.

Ursprüngliche Anfrage zur Bundle-Größe

Für diese Anwendung werden mehr als 80 KB verwendet. Jetzt können Sie herausfinden, 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 neu zu laden, während die Abdeckung erfasst wird.

    App mit Codeabdeckung neu laden

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

    Codeabdeckung des Bundles

Mehr als die Hälfte des Bundles (44 KB) wird nicht einmal verwendet. Das liegt daran, dass ein Großteil des Codes aus Polyfills besteht, damit die Anwendung auch in älteren Browsern funktioniert.

@babel/preset-env verwenden

Die Syntax der JavaScript-Sprache entspricht einem Standard, der als ECMAScript oder ECMA-262 bezeichnet wird. Jedes Jahr werden neue Versionen der Spezifikation veröffentlicht, die neue Funktionen enthalten, die den Vorschlagsprozess durchlaufen haben. Die einzelnen gängigen Browser befinden sich immer in einer anderen Phase der Unterstützung dieser Funktionen.

Die folgenden ES2015-Funktionen werden in der Anwendung verwendet:

Außerdem wird die folgende ES2017-Funktion verwendet:

Sehen Sie sich den Quellcode in src/index.js an, um zu erfahren, wie das alles verwendet wird.

Alle diese Funktionen werden in der aktuellen Version von Chrome unterstützt. Was ist aber mit anderen Browsern, die sie nicht unterstützen? Babel, das in der Anwendung enthalten ist, ist die beliebteste Bibliothek, um Code mit neuerer Syntax in Code zu kompilieren, den ältere Browser und Umgebungen verstehen können. Das geschieht auf zwei Arten:

  • Polyfills werden verwendet, um neuere ES2015+-Funktionen zu emulieren, damit ihre APIs auch dann verwendet werden können, wenn sie vom Browser nicht unterstützt werden. Hier ist ein Beispiel für ein 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 handelt (z. B. Pfeilfunktionen), können sie nicht mit Polyfills emuliert werden.

Sehen Sie sich package.json an, um zu sehen, 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 einer .babelrc im Stammverzeichnis des Projekts definiert.
  • babel-loader enthält Babel im Webpack-Build-Prozess.

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, 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 ermittelt, welche Transformationen und Polyfills für die als Ziele ausgewählten Browser oder Umgebungen erforderlich sind.

Sehen Sie sich die Babel-Konfigurationsdatei .babelrc an, um zu sehen, wie sie eingebunden ist:

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

Dies ist eine Babel- und Webpack-Einrichtung. Informationen zum Einbinden von Babel in Ihre Anwendung, falls Sie einen anderen Modul-Bundler als webpack verwenden.

Das Attribut targets in .babelrc gibt an, auf welche Browser das Targeting ausgerichtet ist. @babel/preset-env ist in browserslist integriert. Eine vollständige Liste der kompatiblen Abfragen, die in diesem Feld verwendet werden können, finden Sie in der browserslist-Dokumentation.

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

Debugging

Wenn Sie sich einen vollständigen Überblick über alle Babel-Ziele des Browsers sowie alle enthaltenen Transformationen und Polyfills verschaffen möchten, fügen Sie .babelrc: ein debug-Feld 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 unten im Editor an.

Zielbrowser

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

Zielbrowser

Beachten Sie, dass eingestellte Browser wie Internet Explorer in dieser Liste enthalten sind. Das ist ein Problem, da nicht unterstützte Browser keine neueren Funktionen erhalten und Babel weiterhin bestimmte Syntax für sie transpiliert. Dies erhöht die Größe Ihres Bundles unnötig, wenn Nutzer nicht diesen Browser verwenden, um auf Ihre Website zuzugreifen.

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

Liste der verwendeten Plug-ins

Das ist eine ziemlich lange Liste. Das sind alle Plug-ins, die Babel benötigt, um die ES2015+-Syntax in eine ältere Syntax für alle Zielbrowser umzuwandeln.

Babel zeigt jedoch keine spezifischen Polyfills an, die verwendet werden:

Keine Polyfills hinzugefügt

Das liegt daran, dass die 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 ein useBuiltIns: 'entry' hinzu.

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

Aktualisieren Sie die Anwendung. Sie können jetzt alle enthaltenen Polyfills sehen:

Liste der importierten Polyfills

Obwohl jetzt nur noch die erforderlichen Polyfills für "last 2 versions" enthalten sind, ist die Liste immer noch sehr lang. Das liegt daran, dass Polyfills, die für die Zielbrowser für jedes neuere Feature erforderlich sind, weiterhin enthalten sind. Ändern Sie den Wert des Attributs in usage, damit nur die Attribute berücksichtigt werden, die für die im Code verwendeten Funktionen erforderlich sind.

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

Dadurch werden Polyfills 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 enthaltenen Polyfills

Die Größe des App-Bundles wird erheblich reduziert.

Bundle-Größ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 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 des abgerufenen Bundles an.

Bundle-Größe von 30,0 KB

Da die Anwendung so klein ist, machen diese Änderungen keinen großen Unterschied. Es wird jedoch empfohlen, einen Prozentsatz für den Marktanteil des Browsers (z. B. ">0.25%") zu verwenden und bestimmte Browser auszuschließen, von denen Sie sicher sind, dass Ihre Nutzer sie nicht verwenden. Weitere Informationen finden Sie im Artikel „Last 2 versions“ considered harmful von James Kyle.

<script type="module"> verwenden

Es gibt aber noch Verbesserungspotenzial. Obwohl eine Reihe ungenutzter Polyfills entfernt wurden, werden viele ausgeliefert, die für einige Browser nicht erforderlich sind. Durch die Verwendung von Modulen kann eine neuere Syntax geschrieben und direkt an Browser gesendet werden, ohne dass unnötige Polyfills verwendet werden müssen.

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 importieren und exportieren. 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 (anstatt Babel zu benötigen). 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 die ein Modul enthält, das weitgehend nicht transpiliert ist, aber eine kleinere Dateigröße hat
  • Eine Version, die ein größeres, transpiliertes Script enthält, das in jedem Legacy-Browser funktioniert

ES-Module mit Babel verwenden

Wenn Sie separate @babel/preset-env-Einstellungen für die beiden Versionen der Anwendung verwenden möchten, entfernen Sie die Datei .babelrc. Babel-Einstellungen können der webpack-Konfiguration hinzugefügt werden, indem für jede Version der Anwendung zwei verschiedene Kompilierungsformate angegeben werden.

Fügen Sie zuerst eine Konfiguration für das Legacy-Script zu 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 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 enthält, um alle Browser zu unterstützen, die ES-Module noch nicht unterstützen.

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 den Legacy-Scripts verwendet, die an den Browser gesendet werden.

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 auf ähnliche Weise ein Konfigurationsobjekt für das 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 Ausgabedateinamen die Dateiendung .mjs verwendet wird. Der Wert esmodules ist hier auf „true“ festgelegt. Das bedeutet, dass der Code, der in dieses Modul ausgegeben wird, ein kleineres, weniger kompiliertes Skript ist, das in diesem Beispiel nicht transformiert wird, da alle verwendeten Funktionen bereits in Browsern unterstützt werden, die Module unterstützen.

Exportieren Sie ganz am Ende der Datei beide Konfigurationen in einem einzigen Array.

module.exports = [
  legacyConfig, moduleConfig
];

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

Browser, die Module unterstützen, ignorieren Skripts mit einem nomodule-Attribut. Browser, die keine Module unterstützen, ignorieren dagegen Skriptelemente mit type="module". Das bedeutet, dass Sie sowohl ein Modul als auch ein kompiliertes Fallback einbinden können. Idealerweise sollten die beiden Versionen der Anwendung so in index.html enthalten sein:

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

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

Im Gegensatz zu regulären Skripts werden Modulskripts standardmäßig immer verzögert. Wenn Sie möchten, dass das entsprechende nomodule-Skript ebenfalls verzögert 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 die Attribute module und nomodule dem Modul bzw. dem Legacy-Script hinzufügen. Importieren Sie 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: ''
    },
    ]
  })
];

Durch diese 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 sowohl die Legacy- als auch die modernen Skriptelemente in die HTML-Datei ausgegeben werden. Leider unterstützt das Plug-in, das die endgültige HTML-Datei erstellt (HTMLWebpackPlugin), derzeit nicht die Ausgabe sowohl des Modul- als auch des Nomodul-Skripts. Es gibt zwar Problemumgehungen und separate Plug-ins, die dieses Problem beheben, z. B. BabelMultiTargetPlugin und HTMLWebpackMultiBuildPlugin, in dieser Anleitung wird jedoch ein einfacherer Ansatz verwendet, bei dem das Modulskriptelement manuell hinzugefügt wird.

Fügen Sie am Ende der Datei Folgendes zu src/index.js 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. in der neuesten Version von Chrome.

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

Es wird nur das Modul abgerufen, das aufgrund der Tatsache, dass es weitgehend nicht transpiliert ist, eine viel kleinere Bundle-Größe hat. 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 ist ein Screenshot aller Anfragen, die mit einer älteren Chrome-Version (Version 38) gestellt wurden.

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

Fazit

Sie wissen jetzt, wie Sie mit @babel/preset-env nur die erforderlichen Polyfills für die Zielbrowser bereitstellen. Sie wissen auch, wie JavaScript-Module die Leistung weiter verbessern können, indem zwei verschiedene transpilierte Versionen einer Anwendung bereitgestellt werden. Mit einem guten Verständnis dafür, wie beide Techniken die Bundle-Größe erheblich reduzieren können, können Sie jetzt mit der Optimierung beginnen.