So vergrößert CommonJS Ihre Bundles

Informationen dazu, wie sich CommonJS-Module auf die Baumstruktur in Ihrer Anwendung auswirken

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

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 von 2009, der Konventionen für JavaScript-Module festgelegt hat. Es war ursprünglich für die Verwendung außerhalb des Webbrowsers vorgesehen, hauptsächlich für serverseitige Anwendungen.

Mit CommonJS können Sie Module definieren, aus ihnen Funktionen 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 der folgenden 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 Nummer 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 JavaScript-Clientbibliotheken auf Clientseite.

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

Da die Größe Ihrer serverseitigen JavaScript-Anwendung nicht so wichtig ist wie im Browser, wurde CommonJS nicht darauf ausgelegt, die Größe des Produktionspakets 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 deiner App zu reduzieren. Sie analysieren Ihre Anwendung zum Zeitpunkt der Erstellung und versuchen, so viel wie möglich aus dem Quellcode zu entfernen, den Sie nicht verwenden.

Im Snippet oben sollte das endgültige 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 die Optimierungen im Produktionsmodus 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

Beachten Sie, dass das Set 625 KB groß ist. Wenn wir uns die Ausgabe ansehen, finden wir alle Funktionen aus utils.js sowie viele Module aus lodash. Wir verwenden lodash zwar nicht in index.js, aber es ist 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);

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

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

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

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

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

Das endgültige Bundle enthält keine der Funktionen von 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 gute Frage, die Sie sich stellen könnten,lautet: Warum ist die Ausgabe bei Verwendung von CommonJS fast 16.000-mal größer? Natürlich ist dies ein Spielzeugbeispiel. Tatsächlich ist der Größenunterschied möglicherweise nicht so groß, aber es besteht die Wahrscheinlichkeit, dass CommonJS Ihrem Produktions-Build erheblichen Gewicht verleiht.

CommonJS-Module sind im Allgemeinen schwieriger zu optimieren, da sie viel dynamischer sind als ES-Module. Damit Ihr Bundler und Ihr Komprimierungsprogramm Ihre Anwendung erfolgreich optimieren können, sollten Sie auf CommonJS-Module verzichten und die ECMAScript-Modulsyntax in Ihrer gesamten Anwendung verwenden.

Wenn Sie ECMAScript-Module in index.js verwenden, es sich aber um ein CommonJS-Modul handelt, leidet die Bundle-Größe Ihrer App.

Warum wird deine App durch CommonJS größer?

Um diese Frage zu beantworten, sehen wir uns das Verhalten von ModuleConcatenationPlugin in webpack an und sprechen danach über die statische Analysebarkeit. Dieses Plug-in verkettet den Bereich aller Ihrer Module zu einem Abschluss, wodurch der Code im Browser schneller ausgeführt werden kann. 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 in index.js importiert wird. Wir definieren auch eine subtract-Funktion. Sie 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 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 überflüssigen Leerzeichen entfernen
  • Inline des Hauptteils der Funktion add in den console.log-Aufruf einfügen

Entwickler bezeichnen diese Entfernung nicht verwendeter Importe häufig als Baumbewältigung. Das Baumwackeln war nur möglich, weil Webpack zum Zeitpunkt der Erstellung statisch verstehen konnte, welche Symbole aus utils.js importiert und welche Symbole exportiert werden.

Dieses Verhalten ist für ES-Module standardmäßig aktiviert, da sie im Vergleich zu CommonJS statisch besser auswertbar 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));

})();

Beachten Sie, dass das letzte Bundle einige webpack „runtime“ enthält: Injizierten Code, der für das Importieren/Exportieren von Funktionen aus den gebündelten Modulen verantwortlich ist. Dieses Mal müssen nicht alle Symbole aus utils.js und index.js unter demselben Namespace platziert werden, sondern müssen zur Laufzeit dynamisch die add-Funktion mit __webpack_require__ verwendet werden.

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 Erstellung nicht den Namen des exportierten Symbols kennen, da hierfür Informationen erforderlich sind, die nur während der 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 durch Baumschütteln wegschütteln. Genauso verhält es sich auch bei Drittanbietermodulen. Wenn wir ein CommonJS-Modul aus node_modules importieren, kann Ihre Build-Toolchain es nicht richtig optimieren.

Baumschuppen mit CommonJS

Es ist viel schwieriger, CommonJS-Module zu analysieren, da sie per Definition dynamisch sind. Beispielsweise ist der Importort in ES-Modulen immer ein Stringliteral, im Gegensatz zu CommonJS, bei dem es sich um einen Ausdruck handelt.

Wenn die von Ihnen verwendete Bibliothek bestimmten Konventionen für die Verwendung von CommonJS folgt, ist es in einigen Fällen möglich, nicht verwendete Exporte bereits beim Erstellen mithilfe eines webpack-Drittanbieter-Plug-ins zu entfernen. Obwohl dieses Plug-in Unterstützung für Tree-Shaking bietet, deckt es nicht alle Möglichkeiten ab, wie Ihre Abhängigkeiten CommonJS verwenden können. Das bedeutet, dass Sie nicht dieselben Garantien erhalten wie für ES-Module. 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 auf CommonJS-Module verzichten und die ECMAScript-Modulsyntax in der gesamten Anwendung verwenden.

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

  • Verwenden Sie die Funktion node-resolve von Rollup.js und legen Sie das Flag modulesOnly fest, um anzugeben, dass Sie nur von ECMAScript-Modulen abhängig sein möchten.
  • Paket is-esm verwenden um zu überprü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.