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

In diesem Codelab verbessern Sie die Leistung dieser einfachen Anwendung, mit der Nutzer zufällige Katzen bewerten können. Informationen zum Optimieren des JavaScript-Bundles durch Minimieren der Menge an transpiliertem Code.

App – Screenshot

In der Beispiel-App können Sie ein Wort oder Emoji auswählen, um auszudrücken, wie sehr Ihnen die jeweilige Katze gefällt. Wenn Sie auf eine Schaltfläche klicken, wird der Wert der Schaltfläche unter dem aktuellen Katzenbild angezeigt.

Messen

Es ist immer eine gute Idee, eine Website zu untersuchen, bevor Sie Optimierungen vornehmen:

  1. Wenn Sie sich eine Vorschau der Website ansehen möchten, drücken Sie App ansehen und dann 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. Klicken Sie das Kästchen Cache deaktivieren an.
  5. Aktualisieren Sie die App.

Ursprüngliche Anfrage zur Bundle-Größe

Für diese Anwendung werden über 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 Coverage (Abdeckung) auf Reload (Neu laden), um die Anwendung neu zu laden und gleichzeitig die Abdeckung zu erfassen.

    App mit Code-Coverage neu laden

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

    Codeabdeckung des Bundles

Über die Hälfte des Bundles (44 KB) wird nicht einmal genutzt. Das liegt daran, dass viel Code darin aus Polyfills besteht, um sicherzustellen, dass die Anwendung in älteren Browsern funktioniert.

@babel/preset-env verwenden

Die Syntax der JavaScript-Sprache entspricht einem Standard namens ECMAScript oder ECMA-262. Jedes Jahr werden neuere Versionen der Spezifikation veröffentlicht, die neue Funktionen enthalten, die das Vorschlagsverfahren durchlaufen haben. Die Unterstützung dieser Funktionen befindet sich in den einzelnen Hauptbrowsern immer in einem anderen Stadium.

In der Anwendung werden die folgenden ES2015-Funktionen verwendet:

Außerdem wird die folgende ES2017-Funktion verwendet:

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

Alle diese Funktionen werden in der aktuellen Version von Chrome unterstützt. Was aber ist mit anderen Browsern, die sie nicht unterstützen? Babel, das in der Anwendung enthalten ist, ist die beliebteste Bibliothek zum Kompilieren von Code, der neuere Syntax enthält, in Code, der von älteren Browsern und Umgebungen verstanden werden kann. Das geschieht auf zwei Arten:

  • Polyfills sind enthalten, 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.
  • Plugins werden verwendet, um ES2015-Code (oder höher) in ältere ES5-Syntax zu transformieren. Da es sich um syntaxbezogene Änderungen handelt (z. B. Pfeilfunktionen), 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 Babel-Kerncompiler. Dadurch werden alle Babel-Konfigurationen in einer .babelrc im Stammverzeichnis des Projekts definiert.
  • babel-loader schließt Babel in den Webpack-Buildprozess ein.

Sehen Sie sich jetzt 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 bietet alle erforderlichen Polyfills für neuere ECMAScript-Funktionen, damit sie in Umgebungen funktionieren, die sie nicht unterstützen. Sie wird 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 wird:

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

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

Das Attribut targets in .babelrc gibt an, auf welche Browser die Ausrichtung erfolgt. @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" transpilieren den Code in der Anwendung für die letzten beiden Versionen jedes Browsers.

Debugging

Wenn Sie sich alle Babel-Ziele des Browsers sowie alle enthaltenen Transformationen und Polyfills ansehen möchten, 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 unten im Editor an.

Browser, auf die ein Targeting möglich ist

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

Browser, auf die ein Targeting möglich ist

Beachten Sie, dass auch eingestellte Browser wie Internet Explorer in dieser Liste enthalten sind. Das ist ein Problem, weil nicht unterstützte Browser keine neuen Funktionen erhalten und Babel weiterhin bestimmte Syntax für sie transpiliert. Dadurch wird die Größe Ihres Bundles unnötig erhöht, wenn Nutzer nicht mit diesem 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. Das sind alle Plug-ins, die Babel benötigt, um ES2015+-Syntax in ältere Syntax für alle Zielbrowser zu transformieren.

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 jedes Polyfill, das für eine vollständige ES2015+-Umgebung erforderlich ist, 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"
      }
    ]
  ]
}

Laden Sie die Anwendung neu. Sie sehen nun alle enthaltenen Polyfills:

Liste der importierten Polyfills

Es werden zwar nur noch die für "last 2 versions" erforderlichen Polyfills einbezogen, aber die Liste ist immer noch sehr lang. Das liegt daran, dass die für die Zielbrowser erforderlichen Polyfills für jede neuere Funktion weiterhin enthalten sind. Ändern Sie den Wert des Attributs in usage, damit nur die für die im Code verwendeten Funktionen erforderlichen Attribute enthalten 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 @babel/polyfill-Import 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 Anwendungs-Bundles wird deutlich reduziert.

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

Einschränkung der Liste der unterstützten Browser

Die Anzahl der enthaltenen Browserziele ist immer noch recht hoch und nur wenige Nutzer verwenden eingestellte Browser wie Internet Explorer. Aktualisieren Sie die Konfigurationen auf Folgendes:

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

Sehen Sie sich die Details für das abgerufene Bundle an.

Bundle-Größe: 30,0 KB

Da die Anwendung so klein ist, macht es keinen großen Unterschied, ob Sie diese Änderungen vornehmen. Es wird jedoch empfohlen, einen Browser-Marktanteil (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 jedoch noch immer Verbesserungsmöglichkeiten. Es wurden zwar einige nicht verwendete Polyfills entfernt, aber es gibt viele, die ausgeliefert werden und für einige Browser nicht erforderlich sind. Durch die Verwendung von Modulen kann 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 von allen wichtigen 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 größtenteils nicht transpiliert wurde, aber eine kleinere Dateigröße hat
  • Eine Version mit einem größeren, transpilierten Skript, 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 haben 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 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
}

Anstelle des targets-Werts für "@babel/preset-env" wird esmodules mit dem Wert false verwendet. Das bedeutet, dass Babel alle erforderlichen Transformationen und Polyfills enthält, um jeden Browser zu unterstützen, der ES-Module noch nicht unterstützt.

Fügen Sie die Objekte entry, cssRule und corePlugins am Anfang der Datei webpack.config.js ein. Diese werden sowohl für das Modul als auch für die Legacy-Skripts 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 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 für den Ausgabedateinamen die Dateiendung .mjs verwendet wird. Der Wert esmodules ist hier auf „true“ gesetzt. Das bedeutet, dass der in diesem Modul ausgegebene Code ein kleineres, weniger kompiliertes Script ist, das in diesem Beispiel keiner Transformation unterzogen wird, da alle verwendeten Funktionen bereits in Browsern unterstützt werden, die Module unterstützen.

Exportieren Sie beide Konfigurationen ganz am Ende der Datei in einem einzelnen 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 Skriptelemente mit type="module". Das bedeutet, dass Sie sowohl ein Modul als auch einen kompilierten Fallback einfügen 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 und 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-Script 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 alten Skript 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 nun 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 allen .mjs-Skriptelementen das Attribut type="module" und allen .js-Skriptmodulen das Attribut nomodule 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 Plugin, mit dem die endgültige HTML-Datei HTMLWebpackPlugin erstellt wird, derzeit nicht die Ausgabe von Modul- und Nomodul-Skripts. Es gibt zwar Problemumgehungen und separate Plug-ins, die dieses Problem lösen, z. B. BabelMultiTargetPlugin und HTMLWebpackMultiBuildPlugin, aber in diesem Tutorial wird ein einfacherer Ansatz verwendet, bei dem das Modulskriptelement manuell hinzugefügt wird.

Fügen Sie am Ende der Datei 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. in der neuesten Version von Chrome.

5,2 KB großes Modul wird für neuere Browser über das Netzwerk abgerufen

Es wird nur das Modul abgerufen, mit einer viel kleineren Bundle-Größe, 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, transpiliertes 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 für die Zielbrowser bereitstellen. Sie wissen auch, wie JavaScript-Module die Leistung weiter verbessern können, indem zwei verschiedene transpilierten Versionen einer Anwendung bereitgestellt werden. Wenn Sie wissen, wie Sie mit diesen beiden Techniken die Größe Ihres Bundles erheblich reduzieren können, können Sie jetzt mit der Optimierung beginnen.