Informationen dazu, wie sich CommonJS-Module auf das Tree Shaking Ihrer Anwendung auswirken
In diesem Beitrag sehen wir uns an, was CommonJS ist und warum es Ihre JavaScript-Bundles unnötig vergrößert.
Zusammenfassung: Damit der Bundler Ihre Anwendung erfolgreich optimieren kann, sollten Sie keine CommonJS-Module verwenden und in der gesamten Anwendung die ECMAScript-Modulsyntax verwenden.
Was ist CommonJS?
CommonJS ist ein Standard aus dem Jahr 2009, der Konventionen für JavaScript-Module festlegte. 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, Funktionen daraus exportieren und in andere Module importieren. Im folgenden Beispiel wird ein Modul definiert, 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 in den frühen 2010er-Jahren 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 Bundle-Größe aus?
Die Größe Ihrer serverseitigen JavaScript-Anwendung ist nicht so kritisch wie im Browser. Daher wurde CommonJS nicht mit dem Ziel entwickelt, die Größe des Produktions-Bundles zu reduzieren. 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 reduzieren. Sie analysieren Ihre Anwendung zur Build-Zeit und versuchen, so viel wie möglich aus dem Quellcode zu entfernen, den Sie nicht verwenden.
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.
Wir erstellen 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 wir Optimierungen im Produktionsmodus verwenden möchten und index.js
als Einstiegspunkt nutzen. Wenn wir nach dem Aufrufen von webpack
die Größe der Ausgabe untersuchen, sehen wir etwa Folgendes:
$ cd dist && ls -lah
625K Apr 13 13:04 out.js
Das Bundle hat eine Größe von 625 KB. Wenn wir uns die Ausgabe ansehen, finden wir alle Funktionen aus utils.js
sowie viele Module aus lodash
. Obwohl wir lodash
nicht in index.js
verwenden, ist es Teil der Ausgabe, was unsere Produktions-Assets unnötig aufbläht.
Ändern wir nun das Modulformat in ECMAScript-Module und versuchen 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);
Mit index.js
würde utils.js
mit der ECMAScript-Modulsyntax importiert:
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 ist jetzt 40 Byte lang und hat die folgende Ausgabe:
(()=>{"use strict";console.log(1+2)})();
Das endgültige Bundle enthält keine der Funktionen aus utils.js
, die wir nicht verwenden, und es gibt keinen Trace von lodash
. Außerdem hat terser
(der JavaScript-Minifier, der von webpack
verwendet wird) die Funktion add
in console.log
inline eingefügt.
Eine berechtigte Frage, die Sie sich stellen könnten,ist: Warum ist das Ausgabebündel bei Verwendung von CommonJS fast 16.000-mal größer? Das ist natürlich nur ein Beispiel. In der Realität ist der Größenunterschied möglicherweise nicht so groß, aber es ist wahrscheinlich, dass CommonJS Ihrem Produktions-Build erhebliches Gewicht hinzufügt.
CommonJS-Module lassen sich im Allgemeinen schwieriger optimieren, da sie viel dynamischer sind als ES-Module. Damit Ihr Bundler und Minifier Ihre Anwendung erfolgreich optimieren können, sollten Sie keine CommonJS-Module verwenden und in Ihrer gesamten Anwendung die ECMAScript-Modulsyntax verwenden.
Auch wenn Sie ECMAScript-Module in index.js
verwenden, wird die Bundle-Größe Ihrer App beeinträchtigt, wenn das verwendete Modul ein CommonJS-Modul ist.
Warum wird meine App durch CommonJS größer?
Um diese Frage zu beantworten, betrachten wir das Verhalten von ModuleConcatenationPlugin
in webpack
und gehen dann auf die statische Analysierbarkeit ein. Mit diesem Plug-in wird der Bereich aller Module in einer Closure zusammengefasst, sodass Ihr 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 wir in index.js
importieren. Außerdem definieren wir 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 erstellte 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 oben genannten Quellcode verarbeitet, geschieht Folgendes:
- Entfernen Sie die nicht verwendeten Funktionen
subtract
undindex_subtract
. - Entferne alle Kommentare und überflüssigen Leerräume.
- Fügen Sie den Text der Funktion
add
in den Aufruf vonconsole.log
ein.
Entwickler bezeichnen das Entfernen nicht verwendeter Importe oft als „Tree Shaking“. Tree-Shaking war nur möglich, weil Webpack statisch (zur Build-Zeit) erkennen konnte, welche Symbole wir aus utils.js
importieren und welche Symbole exportiert werden.
Dieses Verhalten ist für ES-Module standardmäßig aktiviert, da sie im Vergleich zu CommonJS statisch besser analysiert werden können.
Sehen wir uns genau dasselbe Beispiel an, ändern wir aber dieses Mal utils.js
, sodass 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]);
Diese kleine Änderung wird die Ausgabe erheblich verändern. Da es zu lang ist, um es auf dieser Seite einzubetten, 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 endgültige Bundle enthält webpack
-„Laufzeit“: eingefügter Code, der für das Importieren/Exportieren von Funktionen aus den gebündelten Modulen verantwortlich ist. Dieses Mal werden nicht alle Symbole aus utils.js
und index.js
unter demselben Namespace platziert, sondern die add
-Funktion wird dynamisch zur Laufzeit mit __webpack_require__
angefordert.
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 Bundler kann zur Build-Zeit nicht wissen, wie das exportierte Symbol heißt, da dazu Informationen erforderlich sind, die nur zur Laufzeit im Kontext des Nutzerbrowsers verfügbar sind.
So kann der Minifier nicht nachvollziehen, was genau index.js
von seinen Abhängigkeiten verwendet, und es daher nicht entfernen. Das gilt auch für Drittanbietermodule. 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 viel schwieriger zu analysieren, da sie per Definition dynamisch sind. Der Importpfad in ES-Modulen ist beispielsweise immer ein Stringliteral, während er in CommonJS ein Ausdruck ist.
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 zur Build-Zeit mit einem webpack
-Plug-in eines Drittanbieters zu entfernen. Dieses Plug‑in bietet zwar Unterstützung für Tree Shaking, deckt aber nicht alle verschiedenen Möglichkeiten ab, wie Ihre Abhängigkeiten CommonJS verwenden könnten. Das bedeutet, dass Sie nicht die gleichen Garantien wie bei ES-Modulen erhalten. Außerdem fallen im Rahmen des Build-Prozesses zusätzliche Kosten an, die über das Standardverhalten von webpack
hinausgehen.
Fazit
Damit der Bundler Ihre Anwendung erfolgreich optimieren kann, sollten Sie keine CommonJS-Module verwenden und in Ihrer gesamten Anwendung die ECMAScript-Modulsyntax verwenden.
Hier sind einige praktische Tipps, mit denen Sie überprüfen können, ob Sie den optimalen Weg eingeschlagen haben:
- Verwenden Sie das node-resolve-Plugin 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 von Modulen abhängen, die nicht tree-shakeable sind.