So vergrößert CommonJS Ihre Bundles

Informationen zum Einfluss von CommonJS-Modulen auf die Baumstruktur in Ihrer Anwendung

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

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

Was ist CommonJS?

CommonJS ist ein Standard aus dem Jahr 2009, der Konventionen für JavaScript-Module festlegt. Sie wurde ursprünglich für die Verwendung außerhalb des Webbrowsers entwickelt, hauptsächlich für serverseitige Anwendungen.

Mit CommonJS können Sie Module definieren, Funktionen daraus exportieren und 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 die Zahl 3 in der Konsole ausgegeben.

Da es Anfang der 2010er-Jahre kein standardisiertes Modulsystem im Browser gab, wurde CommonJS auch zu einem beliebten Modulformat für clientseitige JavaScript-Bibliotheken.

Wie wirkt sich CommonJS auf die endgültige Paketgröße aus?

Die Größe Ihrer serverseitigen JavaScript-Anwendung ist nicht so kritisch wie im Browser. Daher wurde CommonJS nicht entwickelt, um die Größe des Produktions-Bundles zu reduzieren. Gleichzeitig zeigt die Analyse, dass die Größe des JavaScript-Sets immer noch der Hauptgrund dafür ist, dass Browser-Apps langsamer werden.

JavaScript-Bundler und -Minifier wie webpack und terser führen verschiedene Optimierungen durch, um die Größe Ihrer App zu reduzieren. Dabei wird Ihre Anwendung zum Zeitpunkt des Builds analysiert und es wird versucht, so viel wie möglich aus dem nicht verwendeten Quellcode zu entfernen.

Im obigen Snippet 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 Sie die Anwendung 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 wir Optimierungen im Produktionsmodus verwenden und index.js als Einstiegspunkt verwenden möchten. Wenn wir nach dem Aufrufen von webpack die Größe von output prüfen, sehen wir Folgendes:

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

Das Bundle hat eine Größe von 625 KB. In der Ausgabe sehen wir alle Funktionen aus utils.js sowie viele Module aus lodash. Auch wenn wir lodash nicht in index.js verwenden, ist dies Teil der Ausgabe, wodurch unsere Produktions-Assets stark gewichtet werden.

Ändern Sie 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);

index.js würde dann utils.js mit der ECMAScript-Modulsyntax importieren:

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 hat jetzt 40 Byte mit der folgenden Ausgabe:

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

Das finale Bundle enthält keine der Funktionen aus utils.js, die wir nicht verwenden, und es gibt keine Spur von lodash. terser (der von webpack verwendete JavaScript-Reduzierer) hat die add-Funktion in console.log eingefügt.

Eine berechtigte Frage wäre: Warum ist das Ausgabebundle bei Verwendung von CommonJS fast 16.000-mal größer? Natürlich ist dies ein Beispiel. In der Realität ist der Größenunterschied möglicherweise nicht so groß. Die Chancen stehen jedoch gut, dass CommonJS Ihrem Produktions-Build erheblich mehr Gewicht verleiht.

CommonJS-Module sind im Allgemeinen schwieriger zu optimieren, da sie viel dynamischer sind als ES-Module. Damit Ihr Bundler und Minifier Ihre Anwendung erfolgreich optimieren können, sollten Sie keine Abhängigkeiten von CommonJS-Modulen haben und in Ihrer gesamten Anwendung die ECMAScript-Modulsyntax verwenden.

Auch wenn Sie in index.js ECMAScript-Module verwenden, hat die Größe des Bundles Ihrer App zu leiden, wenn das verwendete Modul ein CommonJS-Modul ist.

Warum macht CommonJS Ihre App größer?

Um diese Frage zu beantworten, sehen wir uns das Verhalten der ModuleConcatenationPlugin in webpack an und sprechen dann über die statische Analysierbarkeit. Dieses Plug-in fasst den Gültigkeitsbereich aller Ihrer Module in einem einzigen Closure zusammen und ermöglicht eine schnellere Ausführungszeit Ihres Codes im Browser. Beispiel:

// 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 wir in index.js importieren. Wir definieren auch eine subtract-Funktion. Wir können das Projekt mit derselben webpack-Konfiguration wie oben erstellen, deaktivieren aber diesmal 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 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 Kollisionen zu vermeiden, hat webpack die subtract-Funktion 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 überflüssigen Leerzeichen entfernen
  • Den Funktionsblock der add-Funktion in den console.log-Aufruf einfügen

Entwickler bezeichnen diese Entfernung nicht verwendeter Importe häufig als Baumbewältigung. Das Tree-Shaking war nur möglich, weil webpack statisch (zur Buildzeit) erkennen konnte, welche Symbole wir aus utils.js importieren und welche Symbole es exportiert.

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

Sehen wir uns genau das gleiche Beispiel an, aber ändern Sie diesmal 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 das Einbetten auf dieser Seite zu lang ist, habe ich nur einen kleinen Teil davon geteilt:

...
(() => {

"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 finale Bundle enthält einige webpack-„Laufzeit“-Elemente: Eingefügter Code, der für den Import/Export von Funktionen aus den gebündelten Modulen verantwortlich ist. Dieses Mal platzieren wir nicht alle Symbole aus utils.js und index.js im selben Namespace, sondern fordern die add-Funktion dynamisch zur Laufzeit mit __webpack_require__ an.

Das ist notwendig, weil 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 Wrapper kann zum Zeitpunkt der Build-Phase nicht wissen, wie das exportierte Symbol heißt, da dafür Informationen erforderlich sind, die erst zur Laufzeit im Kontext des Browsers des Nutzers verfügbar sind.

Auf diese Weise ist der Miniifier nicht in der Lage zu verstehen, was genau index.js aus seinen Abhängigkeiten verwendet, und kann sie nicht wegschütteln. Genau das Gleiche gilt für Module von Drittanbietern. Wenn wir ein CommonJS-Modul aus node_modules importieren, kann es von Ihrer Build-Toolchain nicht richtig optimiert werden.

Tree-Shaking mit CommonJS

CommonJS-Module sind per Definition dynamisch und daher viel schwieriger zu analysieren. Beispielsweise ist der Importort in ES-Modulen immer ein Stringliteral, im Vergleich zu CommonJS, wo es sich um einen Ausdruck handelt.

Wenn die von Ihnen verwendete Bibliothek bestimmten Konventionen für die Verwendung von CommonJS folgt, können Sie in einigen Fällen nicht verwendete Exporte zum Zeitpunkt des Builds mit einem webpack-Plug-in von Drittanbietern entfernen. Dieses Plug-in unterstützt zwar Tree-Shaking, deckt aber nicht alle Möglichkeiten ab, wie Ihre Abhängigkeiten CommonJS verwenden könnten. Das bedeutet, dass Sie nicht dieselben Garantien wie bei ES-Modulen erhalten. Außerdem fallen im Rahmen des Build-Prozesses zusätzliche Kosten an, die über das standardmäßige webpack-Verhalten hinausgehen.

Fazit

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

Hier sind einige praktische Tipps, mit denen Sie überprüfen können, ob Sie auf dem optimalen Weg sind:

  • 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.
  • Verwenden Sie das Paket is-esm, um zu prüfen, ob ein npm-Paket ECMAScript-Module verwendet.
  • Wenn Sie Angular verwenden, erhalten Sie standardmäßig eine Warnung, wenn Sie auf Module angewiesen sind, die nicht baumecht sind.