Comment CommonJS améliore-t-il vos groupes ?

Découvrez l'impact des modules CommonJS sur le tree shaking de votre application.

Dans cet article, nous allons vous expliquer en quoi consiste CommonJS et pourquoi il rend vos groupes JavaScript plus volumineux que nécessaire.

Résumé: Pour vous assurer que le bundler peut optimiser votre application, évitez de dépendre de modules CommonJS et utilisez la syntaxe de module ECMAScript dans l'ensemble de votre application.

Qu'est-ce que CommonJS ?

CommonJS est une norme de 2009 qui établit les conventions pour les modules JavaScript. À l'origine, il était destiné à être utilisé en dehors du navigateur Web, principalement pour les applications côté serveur.

Avec CommonJS, vous pouvez définir des modules, en exporter les fonctionnalités et les importer dans d'autres modules. Par exemple, l'extrait ci-dessous définit un module qui exporte cinq fonctions: add, subtract, multiply, divide et 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]);

Par la suite, un autre module pourra importer et utiliser tout ou partie de ces fonctions:

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

Si vous appelez index.js avec node, le numéro 3 est renvoyé dans la console.

En l'absence de système de modules standardisé dans le navigateur au début des années 2010, CommonJS est devenu un format de module populaire pour les bibliothèques JavaScript côté client.

Comment CommonJS affecte-t-il la taille finale de votre kit ?

La taille de votre application JavaScript côté serveur n'est pas aussi importante que celle du navigateur. C'est pourquoi CommonJS n'a pas été conçu en tenant compte de la taille du bundle de production. Parallèlement, une analyse montre que la taille du bundle JavaScript demeure la première raison du ralentissement des applications de navigateur.

Les outils de compression et de réduction JavaScript, tels que webpack et terser, effectuent différentes optimisations pour réduire la taille de votre application. Lors de l'analyse de votre application au moment de la compilation, ils essaient d'en supprimer un maximum du code source que vous n'utilisez pas.

Par exemple, dans l'extrait ci-dessus, votre bundle final ne doit inclure que la fonction add, car il s'agit du seul symbole de utils.js que vous importez dans index.js.

Créons l'application à l'aide de la configuration webpack suivante:

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

Ici, nous spécifions que nous voulons utiliser les optimisations en mode production et utiliser index.js comme point d'entrée. Après avoir appelé webpack, si nous explorons la taille de output, nous obtenons un résultat semblable à celui-ci:

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

Notez que la taille du bundle est de 625 Ko. En examinant la sortie, nous trouvons toutes les fonctions de utils.js ainsi que de nombreux modules de lodash. Bien que nous n'utilisions pas lodash dans index.js, il fait partie de la sortie, ce qui ajoute beaucoup de poids à nos éléments de production.

Remplacez le format du module par ECMAScript modules (Modules ECMAScript), puis réessayez. Cette fois, utils.js ressemblerait à ceci:

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);

Et index.js effectue l'importation depuis utils.js à l'aide de la syntaxe du module ECMAScript:

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

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

En utilisant la même configuration webpack, nous pouvons compiler l'application et ouvrir le fichier de sortie. Il est maintenant de 40 octets, et la sortie suivante est générée:

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

Notez que le bundle final ne contient aucune des fonctions de utils.js que nous n'utilisons pas et qu'il n'y a aucune trace de lodash. De plus, terser (le minimateur JavaScript utilisé par webpack) a intégré la fonction add dans console.log.

Vous vous demandez peut-être pourquoi l'utilisation de CommonJS entraîne un bundle de sortie presque 16 000 fois plus grand ? Bien sûr, il s'agit d'un exemple de jouet. En réalité, la différence de taille n'est peut-être pas si importante, mais il est probable que CommonJS ajoute un poids important à votre build de production.

Généralement, les modules CommonJS sont plus difficiles à optimiser, car ils sont beaucoup plus dynamiques que les modules ES. Pour vous assurer que votre bundler et votre minimisation peuvent optimiser votre application, évitez de dépendre de modules CommonJS et utilisez la syntaxe du module ECMAScript dans l'ensemble de votre application.

Notez que même si vous utilisez des modules ECMAScript dans index.js, si le module que vous consommez est un module CommonJS, la taille du bundle de votre application en pâtira.

Pourquoi CommonJS est-il agrandi ?

Pour répondre à cette question, nous allons examiner le comportement de ModuleConcatenationPlugin dans webpack, puis nous aborderons l'analyse statique. Ce plug-in concatène le champ d'application de tous vos modules en une seule fermeture et permet d'accélérer l'exécution de votre code dans le navigateur. Voyons un exemple :

// 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));

Ci-dessus, nous avons un module ECMAScript, que nous importons dans index.js. Nous définissons également une fonction subtract. Nous pouvons compiler le projet en utilisant la même configuration webpack que ci-dessus, mais cette fois, nous allons désactiver la minimisation:

const path = require('path');

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

Examinons le résultat obtenu:

/******/ (() => { // 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));**

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

Dans le résultat ci-dessus, toutes les fonctions se trouvent dans le même espace de noms. Pour éviter les conflits, webpack a renommé la fonction subtract dans index.js en index_subtract.

Si un outil de minification traite le code source ci-dessus, il:

  • Supprimez les fonctions inutilisées subtract et index_subtract.
  • Supprimer tous les commentaires et les espaces blancs redondants
  • Intégrer le corps de la fonction add dans l'appel console.log

Les développeurs qualifient souvent la suppression des importations inutilisées de "semblage d'arborescence". L'action de secouer l'arborescence n'était possible que parce que Webpack était capable de comprendre de manière statique (au moment de la compilation) les symboles que nous importons depuis utils.js et ceux qu'il exporte.

Ce comportement est activé par défaut pour les modules ES, car ils sont plus analysables de manière statique par rapport à CommonJS.

Examinons exactement le même exemple, mais cette fois, modifiez utils.js pour qu'il utilise CommonJS au lieu des modules ES:

// 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]);

Cette petite mise à jour modifiera considérablement la sortie. Étant donné qu'elle est trop longue pour être intégrée sur cette page, je n'en ai partagé qu'une petite partie:

...
(() => {

"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));

})();

Notez que le bundle final contient un "runtime" webpack, c'est-à-dire du code injecté chargé d'importer/exporter les fonctionnalités à partir des modules groupés. Cette fois, au lieu de placer tous les symboles de utils.js et index.js sous le même espace de noms, nous avons besoin, de manière dynamique, au moment de l'exécution, de la fonction add avec __webpack_require__.

Cette opération est nécessaire, car CommonJS nous permet d'obtenir le nom de l'exportation à partir d'une expression arbitraire. Par exemple, le code ci-dessous est une construction absolument valide:

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

Au moment de la compilation, le bundler n'a aucun moyen de connaître le nom du symbole exporté, car cela nécessite des informations qui ne sont disponibles qu'au moment de l'exécution, dans le contexte du navigateur de l'utilisateur.

De cette façon, l'outil de minification est incapable de comprendre ce que index.js utilise exactement à partir de ses dépendances. Il ne peut donc pas le secouer l'arbre. Nous observerons exactement le même comportement pour les modules tiers. Si nous importons un module CommonJS depuis node_modules, votre chaîne d'outils de compilation ne pourra pas l'optimiser correctement.

Arborescence avec CommonJS

Il est beaucoup plus difficile d'analyser les modules CommonJS, car ils sont dynamiques par définition. Par exemple, l'emplacement d'importation dans les modules ES est toujours un littéral de chaîne, contrairement à CommonJS où il s'agit d'une expression.

Dans certains cas, si la bibliothèque que vous utilisez respecte des conventions spécifiques concernant CommonJS, vous pouvez supprimer les exportations inutilisées au moment de la compilation à l'aide d'un plugin webpack tiers. Bien que ce plug-in ajoute la prise en charge du tree shaking, il ne couvre pas l'ensemble des différentes façons dont vos dépendances pourraient utiliser CommonJS. Cela signifie que vous ne bénéficiez pas des mêmes garanties qu'avec les modules ES. En outre, en plus du comportement webpack par défaut, cela entraîne des frais supplémentaires dans le cadre de votre processus de compilation.

Conclusion

Pour vous assurer que le bundler peut optimiser votre application, évitez de dépendre de modules CommonJS et utilisez la syntaxe de module ECMAScript dans l'ensemble de votre application.

Voici quelques conseils pratiques pour vérifier que vous êtes sur la bonne voie:

  • Utilisez le plug-in node-resolve de Rollup.js et définissez l'indicateur modulesOnly pour spécifier que vous ne souhaitez dépendre que des modules ECMAScript.
  • Utilisez le package is-esm pour vérifier qu'un package npm utilise des modules ECMAScript.
  • Si vous utilisez Angular, un avertissement s'affichera par défaut si vous dépendez de modules qui ne peuvent pas être transformés en arborescence.