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 Browsermarkts aus. Andere Browser, die auf denselben zugrunde liegenden Rendering-Engines basieren, machen weitere 5 % aus. 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 ist der heutigen Syntax am nächsten.

Veraltetes JavaScript

Legacy-JavaScript ist Code, der ausdrücklich vermeidet, die oben genannten Sprachfunktionen zu verwenden. Die meisten Entwickler schreiben ihren Quellcode mit moderner Syntax, kompilieren aber alles mit der Legacy-Syntax, um eine bessere Browserunterstützung zu erhalten. 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. Mängel bei den Tools 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 ein "exports"-Feld 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 modern

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, z. B. Webpack und Rollup, verwenden dieses Feld, um Modulfunktionen zu nutzen und Baumshaking zu ermöglichen. 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 anzunehmen. 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, auf die Sie angewiesen sind, bereits moderne Sprachfunktionen verwenden. 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 es Abhängigkeiten in dieselbe Syntax wie Ihren Quellcode transpiliert.

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 Sie modernes JavaScript kompilieren und senden können, wobei Legacy-Browser wie das Optimize-Plug-in und BabelEsmPlugin weiterhin unterstützt werden.

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 ein eigenständiges Setup, bei dem in Ihrer Webpack-Konfiguration davon ausgegangen wird, dass alles modernes JavaScript ist, ohne dass spezielle Verzweigungen für mehrere Ausgaben oder Syntaxen vorhanden sind.

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 in der Regel moderner und älterer Code separat gebündelt wird. 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 Standardlösung für „module/nomodule“ und wird von Next.js und der Preact CLI verwendet.

// 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-Loader 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 Sie moderne Sprachfeatures in node_modules automatisch nach ES2017 kompilieren und gleichzeitig Ihren eigenen Code mit den Babel-Plug-ins und -Voreinstellungen, die in der Konfiguration Ihres Projekts definiert sind, übersetzen. 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 Technik, um npm-Abhängigkeiten zu kompilieren, in deren package.json ein "exports"-Feld enthalten ist, 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 die moderne Syntax von Ihrem Minifier unterstützt wird. 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 bietet integrierte Unterstützung für das Generieren mehrerer Sets 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 Methode getBabelOutputPlugin() (im offiziellen Babel-Plug-in von Rollup) den Code in generierten Bundles und nicht in einzelnen Quellmodulen. Rollup bietet integrierte Unterstützung für das Generieren mehrerer Sets von Bundles als Teil eines einzelnen Builds mit jeweils eigenen Plug-ins. 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 hoch konfigurierbar. Das bedeutet im Allgemeinen, dass die Konfiguration jedes Projekts aktualisiert werden muss, um moderne JavaScript-Syntax in Abhängigkeiten zu ermöglichen. Es gibt auch übergeordnete Build-Tools, die Konventionen und Standardeinstellungen gegenüber 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 transformiert, um Legacy-JavaScript-Varianten zu erstellen. Dabei können Bündelungen und Transformationen ein modernes Ausgabeziel annehmen.