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

Leistung verbessern, indem moderne JavaScript-Abhängigkeiten und ‑Ausgaben aktiviert werden

Über 90% der Browser können modernes JavaScript ausführen, aber die Verbreitung von veralteten JavaScript-Versionen ist nach wie vor eine große Quelle für Leistungsprobleme im Web.

Modernes JavaScript

Moderner JavaScript-Code ist nicht durch eine bestimmte ECMAScript-Spezifikationsversion gekennzeichnet, sondern durch eine Syntax, die von allen modernen Browsern unterstützt wird. Moderne Webbrowser wie Chrome, Edge, Firefox und Safari machen mehr als 90% des Browsermarktes aus. Weitere 5 % entfallen auf verschiedene Browser, die auf denselben zugrunde liegenden Rendering-Engines basieren. Das bedeutet, dass 95% des weltweiten Webtraffics von Browsern stammen, die die am häufigsten verwendeten JavaScript-Sprachfunktionen der letzten zehn Jahre unterstützen, darunter:

  • Klassen (ES2015)
  • Pfeilfunktionen (ES2015)
  • Generatoren (ES2015)
  • Block scoping (ES2015)
  • Destrukturierung (ES2015)
  • Parameter für Ruhe und Streuung (ES2015)
  • Objektkürzel (ES2015)
  • Async/await (ES2017)

Funktionen in neueren Versionen der Sprachspezifikation werden in modernen Browsern in der Regel weniger einheitlich unterstützt. Viele ES2020- und ES2021-Funktionen werden beispielsweise nur von 70% der Browser unterstützt. Das ist zwar immer noch die Mehrheit, aber nicht genug, um sich direkt auf diese Funktionen zu verlassen. Das bedeutet, dass „modernes“ JavaScript zwar ein sich ständig änderndes Ziel ist, ES2017 aber die breiteste Browserkompatibilität bietet und gleichzeitig die meisten der gängigen modernen Syntaxfunktionen enthält. Mit anderen Worten: ES2017 kommt der modernen Syntax am nächsten.

Veraltetes JavaScript

Bei Legacy-JavaScript wird bewusst auf die Verwendung aller oben genannten Sprachfunktionen verzichtet. Die meisten Entwickler schreiben ihren Quellcode in moderner Syntax, kompilieren ihn aber für eine bessere Browserunterstützung in die alte Syntax. Das Kompilieren in die alte Syntax erhöht zwar die Browserunterstützung, der Effekt ist jedoch oft geringer als angenommen. In vielen Fällen steigt die Abdeckung von etwa 95 % auf 98 %, was erhebliche Kosten verursacht:

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

  • Installierte Bibliotheken machen bis zu 90% des typischen Produktions-JavaScript-Codes aus. Bibliothekskode verursacht aufgrund von Polyfill- und Hilfsduplikaten einen noch höheren Alt-JavaScript-Overhead, der durch die Veröffentlichung modernen Codes vermieden werden könnte.

Modernes JavaScript auf npm

Kürzlich hat Node.js das Feld "exports" standardisiert, um Einstiegspunkte für ein Paket zu definieren:

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

Module, auf die im Feld "exports" verwiesen wird, erfordern eine Node-Version von mindestens 12.8, die ES2019 unterstützt. Das bedeutet, dass jedes Modul, auf das über das Feld "exports" verwiesen wird, in modernem JavaScript geschrieben werden kann. Paketnutzer müssen davon ausgehen, dass Module mit einem "exports"-Feld modernen Code enthalten, und sie bei Bedarf transpilieren.

Nur moderne

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

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

Modern mit altem Fallback

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

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

Modern mit Legacy-Fallback und ESM-Bundler-Optimierungen

Das Feld "module" kann nicht nur zum Definieren eines CommonJS-Fallback-Eintragspunkts verwendet werden, sondern auch, um auf ein ähnliches Legacy-Fallback-Bundle zu verweisen, 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 nutzen dieses Feld, um Module zu nutzen und Tree Shaking zu aktivieren. Dies ist immer noch ein Legacy-Bundle, das neben der import/export-Syntax keinen modernen Code enthält. Verwenden Sie diesen Ansatz, um modernen Code mit einem Legacy-Fallback zu liefern, der weiterhin für das Bündeln optimiert ist.

Modernes JavaScript in Anwendungen

Abhängigkeiten von Drittanbietern machen den Großteil des typischen Produktions-JavaScript-Codes in Webanwendungen aus. Bisher wurden npm-Abhängigkeiten als alte ES5-Syntax veröffentlicht. Das ist jedoch nicht mehr sicher und es besteht das Risiko, dass durch Abhängigkeitsupdates die Browserunterstützung in Ihrer Anwendung unterbrochen wird.

Da immer mehr npm-Pakete auf moderne JavaScript-Versionen umgestellt werden, ist es wichtig, dass die Build-Tools so eingerichtet sind, dass sie damit umgehen können. Es ist sehr wahrscheinlich, dass einige der npm-Pakete, die Sie verwenden, bereits moderne Sprachfunktionen nutzen. Es gibt eine Reihe von Optionen, um modernen Code von npm zu verwenden, ohne dass Ihre Anwendung in älteren Browsern nicht mehr funktioniert. Im Allgemeinen sollten Sie jedoch das Build-System so konfigurieren, dass Abhängigkeiten in dieselbe Syntax wie Ihr Quellcode transpiliert werden.

webpack

Seit webpack 5 ist es möglich, die Syntax zu konfigurieren, die webpack beim Generieren von Code für Bundles und Module verwendet. Ihr Code oder Ihre Abhängigkeiten werden dadurch nicht transpiliert. Es wirkt sich nur auf den von webpack generierten „Bindungscode“ aus. Um das Ziel der Browserunterstützung anzugeben, fügen Sie Ihrem Projekt eine browserslist-Konfiguration hinzu oder tun Sie dies direkt in Ihrer Webpack-Konfiguration:

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-Modulumgebung unnötige Wrapperfunktionen weglassen. Dadurch wird webpack auch 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 sich modernes JavaScript kompilieren und bereitstellen lässt, während gleichzeitig ältere Browser unterstützt werden, z. B. Optimize Plugin und BabelEsmPlugin.

Optimize-Plug-in

Das Optimize-Plug-in ist ein Webpack-Plug-in, das den finalen gebundelten Code von modernem zu Legacy-JavaScript umwandelt, anstatt jede einzelne Quelldatei. Es ist eine eigenständige Einrichtung, die es Ihrer webpack-Konfiguration ermöglicht, anzunehmen, dass alles modernes JavaScript ist, ohne spezielle Verzweigung für mehrere Ausgaben oder Syntaxen.

Da das Optimize-Plug-in mit Bundles statt mit einzelnen Modulen arbeitet, werden der Code Ihrer Anwendung und Ihre Abhängigkeiten gleichermaßen verarbeitet. So können moderne JavaScript-Abhängigkeiten von npm sicher verwendet werden, da ihr Code gebündelt und in die richtige Syntax transpiliert wird. Außerdem kann es schneller sein als herkömmliche Lösungen mit zwei Kompilierungsschritten, während gleichzeitig separate Bundles für moderne und ältere Browser generiert werden. Die beiden Bündel sind für das Laden mit dem Muster „module/nomodule“ konzipiert.

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

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

Optimize Plugin kann schneller und effizienter sein als benutzerdefinierte Webpack-Konfigurationen, bei denen moderner und älterer Code in der Regel separat gebundelt werden. Außerdem wird Babel für Sie ausgeführt und Bundles werden mit Terser mit separaten optimalen Einstellungen für die modernen und älteren Ausgaben minimiert. Schließlich werden polyfills, die von den generierten Legacy-Bundles benötigt werden, in ein spezielles Script extrahiert, damit sie in neueren Browsern nicht dupliziert oder unnötig geladen werden.

Vergleich: zweimalige Transpilierung von Quellmodulen im Vergleich zur Transpilierung 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 Lösung für „module/nomodule“, die von Next.js und 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 zweimalige Kompilieren kann bei großen Anwendungen etwas mehr Zeit in Anspruch nehmen. Mit dieser Methode lässt sich BabelEsmPlugin jedoch nahtlos in vorhandene Webpack-Konfigurationen einbinden, was sie zu einer der praktischsten Optionen macht.

Babel-Ladeprogramm zum Transpilieren von node_modules konfigurieren

Wenn Sie babel-loader ohne eines der beiden vorherigen Plug-ins verwenden, ist ein wichtiger Schritt erforderlich, um moderne JavaScript-NPM-Module zu verwenden. Wenn Sie zwei separate babel-loader-Konfigurationen definieren, können moderne Sprachfeatures in node_modules automatisch in ES2017 kompiliert werden, während Ihr eigener Code mit den Babel-Plug-ins und ‑Voreinstellungen, die in der Projektkonfiguration definiert sind, weiter transpiliert wird. Dadurch werden keine modernen und älteren Bundles für eine module/nomodule-Einrichtung generiert. Es ist jedoch möglich, npm-Pakete zu installieren und zu verwenden, die modernes JavaScript enthalten, ohne ältere Browser zu beeinträchtigen.

webpack-plugin-modern-npm verwendet diese Methode, um npm-Abhängigkeiten zu kompilieren, die ein "exports"-Feld in ihrer package.json haben, da diese 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 können Sie die Methode manuell in Ihrer Webpack-Konfiguration implementieren, indem Sie beim Auflösen von Modulen im package.json nach einem "exports"-Feld suchen. Ohne Caching 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'],
          },
        },
      },
    ],
  },
};

Bei diesem Ansatz müssen Sie darauf achten, dass Ihr Minifier moderne Syntax unterstützt. Sowohl Terser als auch uglify-es bieten die Möglichkeit, {ecma: 2017} anzugeben, um die ES2017-Syntax während der Komprimierung und Formatierung beizubehalten und in einigen Fällen zu generieren.

Rollup

Rollup unterstützt standardmäßig die Generierung mehrerer Bundle-Sets im Rahmen eines einzelnen Builds und generiert modernen Code. Rollup kann so konfiguriert werden, dass moderne und ältere Bundles mit den offiziellen Plug-ins erstellt werden, die Sie wahrscheinlich bereits verwenden.

@rollup/plugin-babel

Wenn Sie Rollup verwenden, wird der Code mit der Methode getBabelOutputPlugin() (vom offiziellen Babel-Plug-in von Rollup bereitgestellt) in generierten Bundles und nicht in einzelnen Quellmodulen transformiert. Rollup unterstützt das Generieren mehrerer Bundle-Sets im Rahmen eines einzelnen Builds, die jeweils eigene Plug-ins haben. So können Sie unterschiedliche Bundles für moderne und ältere Versionen erstellen, indem Sie jeweils eine andere Babel-Ausgabe-Plug-in-Konfiguration verwenden:

// 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'],
        }),
      ],
    },
  ],
};

Weitere Build-Tools

Rollup und Webpack sind hochgradig konfigurierbar. Das bedeutet in der Regel, dass jedes Projekt seine Konfiguration aktualisieren und die moderne JavaScript-Syntax in Abhängigkeiten aktivieren muss. Es gibt auch Build-Tools der höheren Ebene, die Konventionen und Standardeinstellungen gegenüber der Konfiguration bevorzugen, z. B. Parcel, Snowpack, Vite und WMR. Die meisten dieser Tools gehen davon aus, dass npm-Abhängigkeiten moderne Syntax enthalten können, und transpilieren sie beim Erstellen für die Produktion in die entsprechende Syntaxebene.

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