Saltar al contenido principal




Descubra cómo los módulos de CommonJS están impactando el cambio de árbol de su aplicación


Actualizado

Aparece en:
Tiempos de carga rápidos

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

Resumen: Para asegurarse de que el empaquetador pueda optimizar 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.

¿Qué es CommonJS?

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

Con CommonJS puede definir módulos, exportar la funcionalidad de ellos e importarlos en otros módulos. Por ejemplo, el fragmento a continuación define un módulo que exporta cinco funciones: add, subtract, multiply, dividey max:


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

Más adelante, otro módulo puede importar y utilizar algunas o todas estas funciones:


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

Invocando index.js con node dará salida al número 3 en la consola.

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 cliente de JavaScript.

¿Cómo afecta CommonJS al tamaño final de su paquete?

El tamaño de su aplicación JavaScript del lado del servidor no es tan crítico como en el navegador, por eso CommonJS no fue diseñado para reducir el tamaño del paquete de producción en mente. Al mismo tiempo, análisis muestra que el tamaño del paquete de JavaScript sigue siendo la razón número uno para hacer que las aplicaciones del navegador sean más lentas.

Agrupadores y minificadores de JavaScript, como webpack y 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 sea posible del código fuente que no está utilizando.

Por ejemplo, en el fragmento de arriba, su paquete final solo debe incluir el add función ya que este es el único símbolo de utils.js que importas en index.js.

Construyamos la aplicación usando lo siguiente webpack configuración:

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

Aquí especificamos que queremos usar optimizaciones del modo de producción y usar index.js como punto de entrada. Después de invocar webpack, si exploramos el salida tamaño, veremos algo como esto:

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

Darse cuenta de el paquete es 625KB. Si miramos la salida, encontraremos todas las funciones de utils.js además de muchos módulos de lodash. Aunque no usamos lodash en index.js es parte de la salida, lo que agrega mucho peso adicional a nuestros activos de producción.

Ahora cambiemos el formato del módulo a Módulos ECMAScript e intenta de nuevo. Esta vez, utils.js se vería así:

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

Y index.js importaría de utils.js utilizando la sintaxis del módulo ECMAScript:

import { add } from './utils';

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

Usando el mismo webpack configuración, podemos construir nuestra aplicación y abrir el archivo de salida. Ahora son 40 bytes con lo siguiente salida:

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

Observe que el paquete final no contiene ninguna de las funciones de utils.js que no usamos, y no hay rastro de lodash! Aún más, terser (el minificador de JavaScript que webpack usos) en el add funcionar en console.log.

Una pregunta justa que podrías hacer es: ¿Por qué el uso de CommonJS hace que el paquete de salida sea casi 16.000 veces más grande?? Por supuesto, este es un ejemplo de juguete, en realidad, la diferencia de tamaño puede no ser tan grande, pero lo más probable es que CommonJS agregue un peso significativo a su construcción de producción.

Los módulos CommonJS son más difíciles de optimizar en el caso general porque son mucho más dinámicos que los módulos ES. Para asegurarse de que su agrupador y minificador puedan optimizar 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.

Tenga en cuenta que incluso si está utilizando módulos ECMAScript en index.js, si el módulo que está consumiendo es un módulo CommonJS, el tamaño del paquete de su aplicación se verá afectado.

¿Por qué CommonJS agranda su aplicación?

Para responder a esta pregunta, veremos el comportamiento de ModuleConcatenationPlugin en webpack y, después de eso, discutir la analizabilidad estática. Este complemento concatena el alcance 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 = (a, b) => a + b;
export const subtract = (a, b) => a - b;


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

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

Arriba, tenemos un módulo ECMAScript, que importamos en index.js. También definimos un subtract función. Podemos construir el proyecto usando el mismo webpack configuración como arriba, pero esta vez, desactivaremos la minimización:

const path = require('path');

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

Veamos la salida producida:

 (() => { 
"use strict";


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


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

})();

En el resultado anterior, todas las funciones están dentro del mismo espacio de nombres. Para evitar colisiones, webpack renombró subtract funcionar en index.js a index_subtract.

Si un minificador procesa el código fuente anterior, hará lo siguiente:

  • Eliminar las funciones no utilizadas subtract y index_subtract
  • Eliminar todos los comentarios y los espacios en blanco redundantes
  • Inline el cuerpo del add función en el console.log llamada

A menudo, los desarrolladores se refieren a esto eliminación de importaciones no utilizadas como sacudidas de árboles. La agitación de árboles solo fue posible porque el paquete web pudo comprender estáticamente (en el momento de la compilación) de qué símbolos estamos importando utils.js y qué símbolos exporta.

Este comportamiento está habilitado de forma predeterminada para Módulos ES porque ellos son más analizables estáticamente, en comparación con CommonJS.

Veamos exactamente el mismo ejemplo, pero esta vez cambie utils.js para usar CommonJS en lugar de los módulos ES:


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

Esta pequeña actualización cambiará significativamente la salida. Como es demasiado largo para insertarlo en esta página, solo he compartido una pequeña parte:

...
(() => {

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

})();

Observe que el paquete final contiene algunos webpack «runtime»: código inyectado que es responsable de importar / exportar la funcionalidad de los módulos empaquetados. Esta vez, en lugar de colocar todos los símbolos de utils.js y index.js bajo el mismo espacio de nombres, requerimos dinámicamente, en tiempo de ejecución, el add función usando __webpack_require__.

Esto es necesario porque con CommonJS podemos obtener el nombre de exportación de una expresión arbitraria. Por ejemplo, el siguiente código es una construcción absolutamente válida:

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 usuario.

De esta manera, el minificador es incapaz de entender qué es exactamente index.js utiliza de sus dependencias para que no pueda deshacerse de él. También observaremos exactamente el mismo comportamiento para los módulos de terceros. Si importamos un módulo CommonJS desde node_modules, su cadena de herramientas de compilación no podrá optimizarla correctamente.

Sacudir árboles con CommonJS

Es mucho más difícil analizar los módulos CommonJS ya que son dinámicos por definición. Por ejemplo, la ubicación de importación en los módulos ES es siempre un literal de cadena, en comparación con CommonJS, donde es una expresión.

En algunos casos, si la biblioteca que está usando sigue convenciones específicas sobre cómo usa CommonJS, es posible eliminar las exportaciones no utilizadas en el momento de la compilación utilizando un tercero webpack enchufar. Aunque este complemento agrega soporte para la agitación de árboles, no cubre todas las diferentes formas en que sus dependencias podrían usar CommonJS. Esto significa que no obtiene las mismas garantías que con los módulos ES. Además, agrega un costo adicional como parte de su proceso de construcción además del valor predeterminado webpack comportamiento.

Conclusión

Para asegurarse de que el empaquetador pueda optimizar 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.

A continuación, presentamos algunos consejos prácticos para verificar que se encuentra en el camino óptimo:

  • Utilice Rollup.js’s resolución de nodejs
    plugin y configure el modulesOnly marca para especificar que desea depender solo de los módulos ECMAScript.
  • Usa el paquete is-esm

    para verificar que un paquete npm usa módulos ECMAScript.

  • Si está usando Angular, de forma predeterminada recibirá una advertencia si depende de módulos que no se pueden sacudir en árbol.

R Marketing Digital