Skip to main content




Find out how CommonJS modules are impacting your application tree change


Updated

It appears in:
Fast loading times

En esta publicación, veremos qué es CommonJS y por qué hace que sus paquetes de JavaScript sean más grandes de lo necesario.

Summary: Para asegurarse de que el empaquetador pueda optimize con éxito su aplicación, evite depender de los módulos CommonJS y utilice la sintaxis del módulo ECMAScript en toda su aplicación.

What is CommonJS?

CommonJS es un estándar de 2009 que estableció convenciones para módulos JavaScript. Inicialmente estaba destinado a ser utilizado fuera del browser Web, principalmente para aplicaciones del lado del server.

With CommonJS you can define modules, export their functionality and import them into other modules. For example, the snippet below defines a module that exports five functions: add, subtract, multiply, divideand max:


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

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

Later, another module can import and use some or all of these functions:


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

Invoking index.js with do not give will output the number 3 on the console.

Debido a la falta de un sistema de módulos estandarizado en el navegador a principios de la década de 2010, CommonJS también se convirtió en un formato de módulo popular para las bibliotecas del lado del client de JavaScript.

How does CommonJS affect the final size of your package?

The size of your server-side JavaScript application is not as critical as it is in the browser, which is why CommonJS was not designed to reduce the size of the production package in mind. At the same time, analysis shows that JavaScript packet size is still the number one reason for slowing down browser applications.

JavaScript groupers and minifiers, such as webpack and terser, realice diferentes optimizaciones para reducir el tamaño de su aplicación. Al analizar su aplicación en el momento de la compilación, intentan eliminar tanto como be posible del source code que no está utilizando.

For example, in the snippet above, your final package should only include the add function since this is the only symbol of utils.js what do you matter in index.js.

Let's build the application using the following webpack setting:

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

Here we specify that we want to use production mode optimizations and use index.js as an entry point. After invoking webpack, if we explore the exit size, we will see something like this:

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

Realise package is 625KB. If we look at the output, we will find all the functions of utils.js plus many modules of lodash. Although we do not use lodash in index.js is part of the output, which adds a lot of additional weight to our production assets.

Now let's change the module format to ECMAScript modules and try again. This time, utils.js it would look like this:

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

import { maxBy } desde 'lodash-es';

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

Y index.js would mind utils.js using the ECMAScript module syntax:

import { add } desde './utils';

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

Using the same webpack configuration, we can build our application and open the output file. Now it's 40 bytes with the following exit:

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

Note that the final package does not contain any of the functions of utils.js that we don't use, and there's no trace of lodash! Even more, terser (the JavaScript minifier that webpack uses) in the add run on console.log.

A fair question you could ask is: Why does using CommonJS make the output packet almost 16,000 times larger?? Of course this is a toy example, actually the size difference may not be that great, but CommonJS will most likely add significant weight to your production build.

CommonJS modules are more difficult to optimize in the general case because they are much more dynamic than ES modules. To ensure that your stitcher and minifier can successfully optimize your application, avoid relying on CommonJS modules and use the ECMAScript module syntax throughout your application.

Note that even if you are using ECMAScript modules in index.jsIf the module you are consuming is a CommonJS module, your application's package size will be affected.

Why does CommonJS enlarge its application?

To answer this question, we will look at the behavior of ModuleConcatenationPlugin in webpack y, después de eso, discutir la analizabilidad estática. Este complemento concatena el scope de todos sus módulos en un solo cierre y permite que su código tenga un tiempo de ejecución más rápido en el navegador. Veamos un ejemplo:


export const add = (to, b) => to + b;
export const subtract = (to, b) => to - b;


import { add } desde './utils';
const subtract = (to, b) => to - b;

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

Above, we have an ECMAScript module, which we import into index.js. We also define a subtract function. We can build the project using the same webpack configuration as above, but this time, we will disable minimization:

const path = require('path');

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

Let's see the output produced:

 (() => { 
"use strict";


const add = (to, b) => to + b;
const subtract = (to, b) => to - b;


const index_subtract = (to, b) => to - b;**
console.log(add(1, 2));**

})();

In the above output, all functions are within the same namespace. To avoid collisions, webpack renamed subtract run on index.js to index_subtract.

If a minifier processes the above source code, it will do the following:

  • Delete unused functions subtract and index_subtract
  • Remove all comments and redundant blanks
  • Tilt the body of the add function in the console.log call

This is often referred to by developers removal of unused imports such as tree shaking. Tree shaking was only possible because the web package was able to statically understand (at compile time) what symbols we are importing utils.js and what symbols it exports.

This behavior is enabled by default for ES modules Because they are more statically analyzable, compared to CommonJS.

Let's look at the exact same example, but this time change utils.js to use CommonJS instead of ES modules:


const { maxBy } = require('lodash-es');

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

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

This little update will significantly change the output. Since it is too long to insert on this page, I have only shared a small part:

...
(() => {

"use strict";
var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (to, b) => to - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__ .IH)(1, 2));

})();

Notice that the final package contains some webpack "Runtime": injected code that is responsible for importing / exporting the functionality of the packed modules. This time, instead of placing all the symbols of utils.js and index.js under the same namespace, we dynamically require, at runtime, the add function using __webpack_require__.

This is necessary because with CommonJS we can get the export name from an arbitrary expression. For example, the following code is an absolutely valid construct:

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

No hay forma de que el empaquetador sepa en el tiempo de compilación cuál es el nombre del símbolo exportado, ya que esto requiere información que solo está disponible en tiempo de ejecución, en el contexto del navegador del Username.

In this way, the minifier is unable to understand what exactly is index.js uses its dependencies so you can't get rid of it. We will also observe the exact same behavior for third-party modules. If we import a CommonJS module from node_modules, your build toolchain will not be able to optimize it properly.

Shake trees with CommonJS

CommonJS modules are much more difficult to parse as they are dynamic by definition. For example, the import location in ES modules is always a string literal, compared to CommonJS, where it is an expression.

In some cases, if the library you are using follows specific conventions on how it uses CommonJS, it is possible to remove unused exports at build time using a third party webpack plug. Although this plugin adds support for tree shaking, it doesn't cover all the different ways your dependencies might use CommonJS. This means that you do not get the same guarantees as with ES modules. Also, add an additional cost as part of your build process on top of the default webpack behaviour.

conclusion

To ensure that the packager can successfully optimize your application, avoid relying on CommonJS modules and use the ECMAScript module syntax throughout your application.

Here are some practical tips to verify that you are on the optimal path:

  • Use Rollup.js's nodejs resolution
    plugin and set the modulesOnly checkmark to specify that you want to depend only on ECMAScript modules.
  • Use the package is-esm

    to verify that an npm package uses ECMAScript modules.

  • If you are using Angular, by default you will get a warning if it depends on modules that cannot be tree shaken.