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

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

App – Screenshot

In der Beispiel-App kannst du ein Wort oder ein Emoji auswählen, um zu vermitteln, wie sehr dir jede Katze gefällt. Wenn du auf eine Schaltfläche klickst, zeigt die App den Wert der Schaltfläche unter dem aktuellen Katzenbild an.

Messen

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

  1. Um die Website als Vorschau anzusehen, wählen Sie App ansehen und dann Vollbild Vollbild aus.
  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.

Ursprüngliche Anfrage zur 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 neu zu laden, während die Abdeckung erfasst wird.

    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 Codes aus Polyfills besteht, um sicherzustellen, dass die Anwendung in älteren Browsern funktioniert.

@babel/preset-env verwenden

Die Syntax der JavaScript-Sprache entspricht dem als ECMAScript oder ECMA-262 bekannten Standard. Jedes Jahr werden neuere Versionen der Spezifikation veröffentlicht, die neue Funktionen enthalten, die den Angebotsprozess bestanden haben. Jeder gängige Browser unterstützt diese Funktionen jeweils in unterschiedlichen Phasen.

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 ist die beliebteste Bibliothek, die zum Kompilieren von Code mit neuerer Syntax zu Code verwendet wird, den ältere Browser und Umgebungen verstehen können. Dies geschieht auf zwei Arten:

  • Polyfills sind enthalten, um neuere Funktionen ab 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 Array.includes-Methode.
  • 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 einer .babelrc im Stammverzeichnis des Projekts definiert.
  • babel-loader fügt Babel in den Webpack-Build-Prozess ein.

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

Sehen Sie sich die Babel-Konfigurationsdatei .babelrc an, um ihren Inhalt zu erfahren:

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

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

Das Attribut targets in .babelrc gibt an, welche Browser angesprochen werden. @babel/preset-env lässt sich in die Browserliste integrieren. Das bedeutet, dass Sie in der Dokumentation zur Browserliste eine vollständige Liste der kompatiblen Abfragen, die in diesem Feld verwendet werden können, finden.

Mit dem Wert "last 2 versions" wird der Code in der Anwendung für die letzten beiden Versionen eines 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.

Lade die Anwendung neu und sieh dir am unteren Rand des Editors die Glitch-Statuslogs 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 in nicht unterstützten Browsern keine neueren Funktionen hinzugefügt werden und Babel weiterhin spezifische Syntax für diese transpiliert. Dadurch wird Ihr Bundle unnötig vergrößert, wenn Nutzer nicht diesen Browser 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 ES2015-Syntax für alle Zielbrowser in eine ältere Syntax umzuwandeln.

In Babel werden jedoch keine bestimmten verwendeten Polyfills angezeigt:

Keine Polyfills hinzugefügt

Das liegt daran, dass das gesamte @babel/polyfill-Objekt direkt importiert wird.

Polyfills einzeln laden

Wenn @babel/polyfill in eine Datei importiert wird, enthält Babel standardmäßig jeden Polyfill, der für eine vollständige ES2015+-Umgebung erforderlich ist. Wenn du bestimmte Polyfills importieren möchtest, die für die Zielbrowser erforderlich sind, musst du der Konfiguration ein useBuiltIns: 'entry' hinzufügen.

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

Aktualisieren Sie die Anwendung. Du siehst jetzt alle enthaltenen Polyfills:

Liste der importierten Polyfills

Zwar sind jetzt nur noch benötigte Polyfills für "last 2 versions" enthalten, die Liste ist aber immer noch sehr lang. Das liegt daran, dass die 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, damit nur die Werte berücksichtigt werden, die für im Code verwendete Funktionen erforderlich sind.

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

So 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 eingeschlossenen Polyfills

Die Größe des App-Bundles wurde deutlich 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 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.

Bundle-Größe von 30,0 KB

Da die Anwendung so klein ist, machen diese Änderungen wirklich keinen Unterschied. Es wird jedoch empfohlen, einen Prozentsatz des Browser-Marktanteils (z. B. ">0.25%") zu verwenden und bestimmte Browser auszuschließen, von 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">

Es gibt noch mehr Raum für Verbesserungen. Obwohl eine Reihe nicht verwendeter Polyfills entfernt wurde, gibt es viele, die ausgeliefert werden, die in einigen Browsern nicht benötigt werden. Mithilfe von Modulen kann neuere Syntax ohne unnötige Polyfills geschrieben und an Browser gesendet 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 (anstatt Babel zu benötigen). Die Babel-Konfiguration kann also so geändert werden, 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 wurde, aber eine kleinere Dateigröße hat
  • Version mit einem größeren, transpilierten Skript, das in jedem älteren Browser funktionieren kann

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 Legacy-Skript 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
}

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

Fügen Sie am Anfang der Datei webpack.config.js die Objekte entry, cssRule und corePlugins hinzu. Sie werden sowohl von dem 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 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 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 einzelnes 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, hingegen ignorieren 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 im Gegensatz zu regulären Skripts standardmäßig immer verzögert werden. 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 noch die Attribute module und nomodule dem Modul bzw. dem Legacy-Skript hinzufügen und das ScriptExtHtmlWebpackPlugin ganz oben in webpack.config.js importieren:

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 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.

Module im HTML-Dokument bereitstellen

Als Letztes müssen Sie noch die alten und die modernen Skriptelemente in die HTML-Datei ausgeben. Leider unterstützt das Plug-in, das die endgültige HTML-Datei HTMLWebpackPlugin erstellt, derzeit nicht die Ausgabe sowohl des Moduls als auch des Nomodule-Skripts. Obwohl es Behelfslösungen und separate Plug-ins zur Lösung dieses Problems gibt, z. B. BabelMultiTargetPlugin und HTMLWebpackMultiBuildPlugin, wird für diese Anleitung eine einfachere Methode verwendet, das Modulskriptelement manuell hinzuzufügen.

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 nun die Anwendung in einem Browser, der Module unterstützt, z. B. die aktuelle Version von Chrome.

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

Es wird nur das Modul abgerufen. Da das Modul größtenteils nicht transpiliert wurde, ist die Bundle-Größe viel geringer. 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 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. Sie wissen auch, wie Sie die Leistung mit JavaScript-Modulen weiter verbessern können, indem Sie zwei verschiedene transpilierte Versionen einer Anwendung versenden. Wenn Sie genau wissen, wie Sie mit beiden Methoden die Bundle-Größe erheblich reduzieren können, können Sie loslegen und optimieren.