So vergrößert CommonJS Ihre Bundles

Informationen dazu, wie sich CommonJS-Module auf das TreeShaking Ihrer Anwendung auswirken

In diesem Post erfahren Sie, was CommonJS ist und warum Ihre JavaScript-Bundles größer als nötig werden.

Zusammenfassung: Damit der Bundler Ihre Anwendung erfolgreich optimieren kann, sollten Sie die Abhängigkeit von CommonJS-Modulen vermeiden und die ECMAScript-Modulsyntax in Ihrer gesamten Anwendung verwenden.

Was ist CommonJS?

CommonJS ist ein Standard aus dem Jahr 2009, der Konventionen für JavaScript-Module festgelegt hat. Es war ursprünglich für die Verwendung außerhalb des Webbrowsers vorgesehen, vor allem für serverseitige Anwendungen.

Mit CommonJS können Sie Module definieren, Funktionen aus ihnen exportieren und sie in andere Module importieren. Das folgende Snippet definiert beispielsweise ein Modul, das fünf Funktionen exportiert: add, subtract, multiply, divide und max:

// utils.js
const { maxBy } = require('lodash-es');
const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};

Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

Später kann ein anderes Modul einige oder alle dieser Funktionen importieren und verwenden:

// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));

Wenn Sie index.js mit node aufrufen, wird in der Konsole die Zahl 3 ausgegeben.

Aufgrund des Mangels an standardisiertem Modulsystem im Browser Anfang der 2010er wurde CommonJS auch zu einem beliebten Modulformat für clientseitige JavaScript-Bibliotheken.

Wie wirkt sich CommonJS auf die endgültige Bundle-Größe aus?

Die Größe Ihrer serverseitigen JavaScript-Anwendung ist nicht so kritisch wie die des Browsers. Deshalb wurde CommonJS nicht mit Blick auf die Reduzierung der Größe des Produktions-Bundles entwickelt. Gleichzeitig zeigt die Analyse, dass die Größe des JavaScript-Bundles immer noch der Hauptgrund für die Verlangsamung von Browser-Apps ist.

JavaScript-Bundler und -Minifier wie webpack und terser führen verschiedene Optimierungen durch, um die Größe Ihrer App zu verringern. Bei der Analyse Ihrer App zum Build-Zeitpunkt wird versucht, so viel wie möglich aus dem Quellcode zu entfernen, den Sie nicht verwenden.

Im Snippet oben sollte Ihr endgültiges Bundle beispielsweise nur die Funktion add enthalten, da dies das einzige Symbol aus utils.js ist, das Sie in index.js importieren.

Erstellen wir die App mit der folgenden webpack-Konfiguration:

const path = require('path');
module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production',
};

Hier geben wir an, dass die Optimierungen im Produktionsmodus verwendet werden sollen, und verwenden index.js als Einstiegspunkt. Wenn wir nach dem Aufrufen von webpack die Größe output untersuchen, erhalten wir eine Ausgabe wie die folgende:

$ cd dist && ls -lah
625K Apr 13 13:04 out.js

Wie Sie sehen, ist das Bundle 625 KB groß. In der Ausgabe finden wir alle Funktionen von utils.js sowie zahlreiche Module von lodash. Obwohl wir lodash in index.js nicht verwenden, ist es ein Teil der Ausgabe, was unsere Produktions-Assets erheblich aufwertet.

Ändern wir nun das Modulformat in ECMAScript-Module und versuchen Sie es noch einmal. Dieses Mal würde utils.js so aussehen:

export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

import { maxBy } from 'lodash-es';

export const max = arr => maxBy(arr);

Und index.js würde mithilfe der ECMAScript-Modulsyntax aus utils.js importiert werden:

import { add } from './utils.js';

console.log(add(1, 2));

Mit derselben webpack-Konfiguration können wir unsere Anwendung erstellen und die Ausgabedatei öffnen. Sie beträgt jetzt 40 Byte mit der folgenden Ausgabe:

(()=>{"use strict";console.log(1+2)})();

Das endgültige Bundle enthält keine Funktionen aus utils.js, die nicht von uns verwendet werden, und es gibt keinen Trace von lodash. Darüber hinaus hat terser (der von webpack verwendete JavaScript-Minifier) die add-Funktion in console.log eingefügt.

Eine gute Frage könnte lauten: Warum führt die Verwendung von CommonJS dazu, dass das Ausgabe-Bundle fast 16.000-mal größer ist? Natürlich ist dies ein Beispiel für Spielzeug. In Wirklichkeit ist der Größenunterschied vielleicht nicht so groß, aber die Chancen stehen gut, dass CommonJS Ihren Produktions-Build erheblich aufwendet.

CommonJS-Module lassen sich im Allgemeinen schwieriger optimieren, da sie wesentlich dynamischer als ES-Module sind. Damit dein Bundler und Minifier deine Anwendung erfolgreich optimieren kann, solltest du die Abhängigkeit von CommonJS-Modulen vermeiden und in der gesamten Anwendung die ECMAScript-Modulsyntax verwenden.

Wenn es sich bei dem verwendeten Modul um ein CommonJS-Modul handelt, wird die Bundle-Größe deiner App beeinträchtigt, auch wenn du ECMAScript-Module in index.js verwendest.

Warum macht CommonJS deine App größer?

Zur Beantwortung dieser Frage schauen wir uns das Verhalten von ModuleConcatenationPlugin in webpack an und erörtern anschließend die statische Auswertbarkeit. Dieses Plug-in verkettet den Umfang aller Ihrer Module zu einem Abschluss und ermöglicht eine schnellere Ausführung des Codes im Browser. Sehen wir uns ein Beispiel an:

// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;

console.log(add(1, 2));

Oben sehen Sie ein ECMAScript-Modul, das in index.js importiert wird. Wir definieren auch eine subtract-Funktion. Wir können das Projekt mit derselben webpack-Konfiguration wie oben erstellen, aber dieses Mal deaktivieren wir die Minimierung:

const path = require('path');

module.exports = {
  entry: 'index.js',
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    minimize: false
  },
  mode: 'production',
};

Sehen wir uns die erzeugte Ausgabe an:

/******/ (() => { // webpackBootstrap
/******/    "use strict";

// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**

/******/ })();

In der obigen Ausgabe befinden sich alle Funktionen im selben Namespace. Um Konflikte zu vermeiden, hat Webpack die Funktion subtract in index.js in index_subtract umbenannt.

Wenn ein Minifier den obigen Quellcode verarbeitet, geschieht Folgendes:

  • Entfernen Sie die nicht verwendeten Funktionen subtract und index_subtract.
  • Alle Kommentare und redundanten Leerzeichen entfernen
  • Inline-Text der add-Funktion im console.log-Aufruf einfügen

Oft bezeichnen Entwickler das Entfernen nicht verwendeter Importe als Baumwolken. Baumwackeln war nur möglich, weil Webpack (zum Zeitpunkt der Erstellung) statisch verstehen konnte, welche Symbole wir aus utils.js importieren und welche Symbole exportiert werden.

Dieses Verhalten ist standardmäßig für ES-Module aktiviert, da sie im Vergleich zu CommonJS statischer auswertbarer sind.

Sehen wir uns das exakte Beispiel an, ändern aber utils.js so, dass CommonJS anstelle von ES-Modulen verwendet wird:

// utils.js
const { maxBy } = require('lodash-es');

const fns = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b,
  max: arr => maxBy(arr)
};

Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);

Dieses kleine Update wird die Ausgabe erheblich verändern. Da es zu lang zum Einbetten auf dieser Seite ist, habe ich nur einen kleinen Teil davon freigegeben:

...
(() => {

"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));

})();

Das letzte Bundle enthält Code „runtime“: injizierter webpack, der für den Import/Export von Funktionen aus den gebündelten Modulen verantwortlich ist. Dieses Mal wird die add-Funktion mithilfe von __webpack_require__ zur Laufzeit dynamisch benötigt, anstatt alle Symbole aus utils.js und index.js unter demselben Namespace zu platzieren.

Dies ist notwendig, da wir mit CommonJS den Exportnamen aus einem beliebigen Ausdruck abrufen können. Der folgende Code ist beispielsweise ein absolut gültiges Konstrukt:

module.exports[localStorage.getItem(Math.random())] = () => { … };

Der Bundler kann bei der Build-Erstellung nicht wissen, wie der Name des exportierten Symbols lautet, da dieser Informationen Informationen erfordert, die nur während der Laufzeit und im Kontext des Browsers des Nutzers verfügbar sind.

Auf diese Weise kann der Minifier nicht verstehen, was genau index.js anhand seiner Abhängigkeiten verwendet, sodass es nicht durch Baumstrukturen geschüttelt werden kann. Dasselbe Verhalten wird auch für Module von Drittanbietern beobachtet. Wenn wir ein CommonJS-Modul aus node_modules importieren, kann Ihre Build-Toolchain es nicht richtig optimieren.

Baumwolken mit CommonJS

Es ist viel schwieriger, CommonJS-Module zu analysieren, da sie per Definition dynamisch sind. Zum Beispiel ist der Importort in ES-Modulen immer ein Stringliteral, im Gegensatz zu CommonJS, wo er ein Ausdruck ist.

Wenn die von Ihnen verwendete Bibliothek bestimmten Konventionen bezüglich der Verwendung von CommonJS folgt, ist es in einigen Fällen möglich, nicht verwendete Exporte zum Build-Zeitpunkt mit dem webpack-plugin eines Drittanbieters zu entfernen. Obwohl dieses Plug-in Unterstützung für Tree Shaking bietet, deckt es nicht alle verschiedenen Möglichkeiten ab, wie Ihre Abhängigkeiten CommonJS verwenden können. Das bedeutet, dass Sie nicht die gleichen Garantien wie bei ES-Modulen erhalten. Zusätzlich zum Standardverhalten webpack fallen im Rahmen Ihres Build-Prozesses zusätzliche Kosten an.

Fazit

Damit der Bundler Ihre Anwendung erfolgreich optimieren kann, sollten Sie die Abhängigkeit von CommonJS-Modulen vermeiden und die ECMAScript-Modulsyntax in Ihrer gesamten Anwendung verwenden.

Hier sind ein paar praktische Tipps, mit denen Sie überprüfen können, ob alles in Ordnung ist:

  • Verwenden Sie das node-resolve-Plug-in von Rollup.js und legen Sie das Flag modulesOnly fest, um anzugeben, dass Sie nur von ECMAScript-Modulen abhängig sein möchten.
  • Mit dem Paket is-esm können Sie prüfen, ob ein npm-Paket ECMAScript-Module verwendet.
  • Wenn Sie Angular verwenden, erhalten Sie standardmäßig eine Warnung, wenn Sie von Modulen abhängig sind, die nicht durch Baumstruktur schüttelt werden können.