Modernen JavaScript-Code für schnellere Anwendungen veröffentlichen, versenden und installieren

Sie können die Leistung verbessern, indem Sie moderne JavaScript-Abhängigkeiten und -Ausgaben aktivieren.

Über 90% der Browser können modernes JavaScript ausführen, aber die Verbreitung von Legacy-JavaScript bleibt eine große Ursache für Leistungsprobleme im Web.

Modernes JavaScript

Modernes JavaScript ist nicht als Code gekennzeichnet, der in einer bestimmten ECMAScript-Spezifikationsversion geschrieben ist, sondern als Syntax, die von allen modernen Browsern unterstützt wird. Moderne Webbrowser wie Chrome, Edge, Firefox und Safari machen mehr als 90% des Browsermarkts aus. Weitere 5 % aller Browser, die dieselben zugrunde liegenden Rendering-Engines nutzen, machen weitere Browser aus. Das bedeutet, dass 95% der Webzugriffe weltweit über Browser erfolgen, die die am häufigsten verwendeten JavaScript-Sprachfunktionen der letzten 10 Jahre unterstützen, darunter:

  • Klassen (ES2015)
  • Pfeilfunktionen (ES2015)
  • Generatoren (ES2015)
  • Blockierungsumfang (ES2015)
  • Vernichtung (ES2015)
  • Ruhe- und Streu-Parameter (ES2015)
  • Objekt-Kurzschreibweise (ES2015)
  • Async/await (ES2017)

Features in neueren Versionen der Sprachspezifikation werden von den modernen Browsern in der Regel weniger konsistent unterstützt. Beispielsweise werden viele ES2020- und ES2021-Funktionen nur in 70% des Browsermarkts unterstützt. Auch wenn die meisten Browser zwar die meisten Browser nutzen, ist es jedoch nicht sicher, sich direkt auf diese Funktionen zu verlassen. Obwohl „modernes“ JavaScript ein bewegliches Ziel ist, bietet ES2017 die größte Browserkompatibilität und beinhaltet gleichzeitig die meisten der gängigen modernen Syntaxfunktionen. Mit anderen Worten: ES2017 entspricht heute der modernen Syntax.

Altes JavaScript

Bei Legacy-JavaScript wird die Verwendung der oben genannten Sprachfunktionen explizit vermeidet. Die meisten Entwickler schreiben ihren Quellcode mit moderner Syntax, kompilieren jedoch alles in die Legacy-Syntax, um die Browserunterstützung zu verbessern. Die Kompilierung mit Legacy-Syntax erhöht die Browserunterstützung, der Effekt ist jedoch oft geringer, als wir denken. In vielen Fällen erhöht sich der Support von etwa 95 % auf 98 %, wobei erhebliche Kosten anfallen:

  • Legacy-JavaScript ist in der Regel etwa 20% größer und langsamer als entsprechender moderner Code. Toolmängel und Fehlkonfigurationen vergrößern diese Lücke oft noch weiter.

  • Installierte Bibliotheken machen 90% des typischen JavaScript-Produktionscodes aus. Bibliothekscode verursacht aufgrund von Polyfill- und Hilfsvervielfältigung einen noch höheren Legacy-JavaScript-Aufwand, der durch die Veröffentlichung von modernem Code vermieden werden könnte.

Modernes JavaScript auf npm

Seit Kurzem wird in Node.js das Feld "exports" standardisiert, mit dem Einstiegspunkte für ein Paket definiert werden:

{
  "exports": "./index.js"
}

Module, auf die im Feld "exports" verwiesen wird, implizieren eine Knotenversion von mindestens 12.8, die ES2019 unterstützt. Somit kann jedes Modul, auf das mit dem Feld "exports" verwiesen wird, in modernem JavaScript geschrieben werden. Bei Paketnutzern muss davon ausgegangen werden, dass Module mit einem Feld "exports" modernen Code enthalten und bei Bedarf Transpiler ausgeführt werden.

Nur modern

Wenn Sie ein Paket mit modernem Code veröffentlichen und die Transpilerung dem Nutzer überlassen möchten, wenn er es als Abhängigkeit verwendet, verwenden Sie nur das Feld "exports".

{
  "name": "foo",
  "exports": "./modern.js"
}

Modern mit Legacy-Fallback

Verwenden Sie das Feld "exports" zusammen mit "main", um Ihr Paket mit modernem Code zu veröffentlichen, aber auch ein ES5- und CommonJS-Fallback für Legacy-Browser einbinden.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Modern mit Legacy-Fallback- und ESM-Bundler-Optimierungen

Neben der Definition eines CommonJS-Fallback-Einstiegspunkts kann mit dem Feld "module" auf ein ähnliches Legacy-Fallback-Bundle verwiesen werden, das jedoch die JavaScript-Modulsyntax (import und export) verwendet.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Viele Bundler wie Webpack und Rollup verlassen sich auf dieses Feld, um Modulfeatures zu nutzen und Tree Shaking zu aktivieren. Dies ist immer noch ein Legacy-Bundle, das abgesehen von der import/export-Syntax keinen modernen Code enthält. Verwenden Sie diesen Ansatz, um modernen Code mit einem Legacy-Fallback zu senden, das weiterhin für die Bündelung optimiert ist.

Modernes JavaScript in Anwendungen

Abhängigkeiten von Drittanbietern machen den Großteil des typischen JavaScript-Produktionscodes in Webanwendungen aus. npm-Abhängigkeiten wurden in der Vergangenheit als Legacy-ES5-Syntax veröffentlicht. Dies ist jedoch keine sichere Annahme mehr und es besteht kein Risiko mehr, dass Abhängigkeitsaktualisierungen die Browserunterstützung in Ihrer Anwendung beeinträchtigen.

Immer mehr npm-Pakete werden auf modernes JavaScript umgestellt. Deshalb ist es wichtig, dass die Build-Tools entsprechend eingerichtet sind. Es ist sehr wahrscheinlich, dass einige der npm-Pakete, von denen Sie abhängig sind, bereits moderne Sprachfunktionen verwenden. Es gibt eine Reihe von Optionen, um modernen Code aus npm zu verwenden, ohne Ihre Anwendung in älteren Browsern zu unterbrechen. Generell ist es aber, das Build-System Abhängigkeiten in dasselbe Syntaxziel wie Ihr Quellcode transpilieren zu lassen.

Webpack

Ab Webpack 5 ist es jetzt möglich zu konfigurieren, welche Syntax Webpack beim Generieren von Code für Bundles und Module verwendet. Dadurch werden Ihr Code oder Ihre Abhängigkeiten nicht transpiliert. Es wirkt sich nur auf den von Webpack generierten „Glue“-Code aus. Wenn Sie das Browserunterstützungsziel angeben möchten, fügen Sie Ihrem Projekt eine Browserlistenkonfiguration hinzu oder geben dies direkt in der Webpack-Konfiguration an:

module.exports = {
  target: ['web', 'es2017'],
};

Es ist auch möglich, Webpack so zu konfigurieren, dass optimierte Bundles generiert werden, die bei der Ausrichtung auf eine moderne ES Modules-Umgebung unnötige Wrapper-Funktionen weglassen. Dadurch wird Webpack außerdem so konfiguriert, dass Code-Split-Bundles mit <script type="module"> geladen werden.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Es gibt eine Reihe von Webpack-Plug-ins, mit denen Sie modernes JavaScript kompilieren und ausliefern können, ohne dabei Legacy-Browser zu unterstützen, z. B. das Optimize-Plug-in und BabelEsmPlugin.

Optimize-Plug-in

Das Optimize-Plug-in ist ein Webpack-Plug-in, das den endgültigen gebündelten Code von modernem in Legacy-JavaScript und nicht mehr jede einzelne Quelldatei umwandelt. Es ist ein eigenständiges Setup, bei dem Ihre Webpack-Konfiguration davon ausgeht, dass alles modernes JavaScript ist, ohne spezielle Verzweigungen für mehrere Ausgaben oder Syntaxen.

Da das Optimize-Plug-in mit Bundles und nicht mit einzelnen Modulen ausgeführt wird, werden der Code der Anwendung und Ihre Abhängigkeiten gleich verarbeitet. Dies macht es sicher, moderne JavaScript-Abhängigkeiten von npm zu verwenden, da ihr Code gebündelt und in die richtige Syntax transpiliert wird. Sie kann auch schneller sein als herkömmliche Lösungen, die zwei Kompilierungsschritte erfordern, dabei aber separate Bundles für moderne und Legacy-Browser generieren. Die beiden Sets sind so konzipiert, dass sie nach dem Muster für Module/nomodule geladen werden.

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin kann schneller und effizienter sein als benutzerdefinierte Webpack-Konfigurationen, in denen modernen und Legacy-Code in der Regel separat gebündelt werden. Außerdem führt er Babel für Sie aus und komprimiert Bundles mit Terser mit separaten optimalen Einstellungen für die moderne und die Legacy-Ausgabe. Schließlich werden die von den generierten Legacy-Bundles benötigten Polyfills in ein spezielles Skript extrahiert, damit sie nie dupliziert oder in neueren Browsern unnötigerweise geladen werden.

Vergleich: Quellmodule zweimal transpilieren im Vergleich zum Transpilieren generierter Bundles.

BabelEsmPlugin

BabelEsmPlugin ist ein Webpack-Plug-in, das zusammen mit @babel/preset-env moderne Versionen vorhandener Bundles generiert, um weniger transpilierten Code an moderne Browser zu senden. Es ist die beliebteste Standardlösung für module/nomodule, die von Next.js und der Preact CLI verwendet wird.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin unterstützt eine Vielzahl von Webpack-Konfigurationen, da zwei weitgehend separate Builds Ihrer Anwendung ausgeführt werden. Das doppelte Kompilieren kann bei großen Anwendungen etwas mehr Zeit in Anspruch nehmen. Mit dieser Methode kann BabelEsmPlugin jedoch nahtlos in vorhandene Webpack-Konfigurationen eingebunden werden und ist damit eine der praktischsten verfügbaren Optionen.

Babelloader für das Transpilieren von Knotenmodulen konfigurieren

Wenn Sie babel-loader ohne eines der beiden vorherigen Plug-ins verwenden, ist ein wichtiger Schritt erforderlich, um moderne JavaScript-npm-Module nutzen zu können. Wenn Sie zwei separate babel-loader-Konfigurationen definieren, können Sie automatisch moderne Sprachfunktionen aus node_modules in ES2017 kompilieren und gleichzeitig Ihren eigenen Code mit den Babel-Plug-ins und Voreinstellungen, die in der Konfiguration Ihres Projekts definiert sind, transpilieren. Dadurch werden keine modernen und Legacy-Bundles für eine Modul-/Nomodule-Einrichtung generiert, aber es ist möglich, npm-Pakete mit modernem JavaScript zu installieren und zu verwenden, ohne ältere Browser zu beeinträchtigen.

webpack-plugin-modern-npm verwendet diese Technik, um npm-Abhängigkeiten zu kompilieren, deren package.json ein "exports"-Feld enthält, da diese eine moderne Syntax enthalten können:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

Alternativ kannst du das Verfahren manuell in deiner Webpack-Konfiguration implementieren. Dazu suche in der package.json der Module nach dem Feld "exports", sobald die Module aufgelöst wurden. Ohne das Caching der Kürzel könnte eine benutzerdefinierte Implementierung so aussehen:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

Wenn Sie diesen Ansatz verwenden, müssen Sie dafür sorgen, dass die moderne Syntax von der Reduzierung unterstützt wird. Sowohl Terser als auch uglify-es haben eine Option zur Angabe von {ecma: 2017}, um die ES2017-Syntax während der Komprimierung und Formatierung beizubehalten und in einigen Fällen zu generieren.

Alle Daten auf einen Blick

Rollup unterstützt das Generieren mehrerer Gruppen von Bundles als Teil eines einzelnen Builds und generiert standardmäßig modernen Code. Daher kann Rollup so konfiguriert werden, dass moderne und Legacy-Bundles mit den offiziellen Plug-ins generiert werden, die Sie wahrscheinlich bereits verwenden.

@rollup/plugin-babel

Wenn Sie Rollup verwenden, transformiert die getBabelOutputPlugin()-Methode (vom offiziellen Babel-Plug-in von Rollup bereitgestellt) den Code in generierte Bundles und nicht in einzelne Quellmodule. Rollup unterstützt das Generieren mehrerer Gruppen von Bundles als Teil eines einzelnen Builds mit jeweils eigenen Plug-ins. Sie können dies verwenden, um verschiedene Bundles für moderne und Legacy-Pakete zu erstellen. Dazu übergeben Sie jedes eine andere Konfiguration des Babel-Ausgabe-Plug-ins:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

Zusätzliche Build-Tools

Rollup und Webpack sind hochkonfigurierbar. Das bedeutet im Allgemeinen, dass jedes Projekt seine Konfiguration aktualisieren muss, um die moderne JavaScript-Syntax in den Abhängigkeiten zu ermöglichen. Es gibt auch übergeordnete Build-Tools, die Konventionen und Standardkonfigurationen vorziehen, wie Parcel, Snowpack, Vite und WMR. Die meisten dieser Tools gehen davon aus, dass npm-Abhängigkeiten eine moderne Syntax enthalten können, und transpiliert sie beim Erstellen für die Produktion in die entsprechenden Syntaxebenen.

Zusätzlich zu den speziellen Plug-ins für Webpack und Rollup können mithilfe von Entwicklung jedem Projekt moderne JavaScript-Bundles mit Legacy-Fallbacks hinzugefügt werden. Devolution ist ein eigenständiges Tool, das die Ausgabe eines Build-Systems in die Erstellung von Legacy-JavaScript-Varianten umwandelt. Dadurch können Bündelung und Transformationen von einem modernen Ausgabeziel angenommen werden.