Las aplicaciones web actuales pueden ser bastante grandes, en especial la parte de JavaScript. A mediados de 2018, HTTP Archive estableció el tamaño de transferencia promedio de JavaScript en dispositivos móviles en aproximadamente 350 KB. Esto es solo el tamaño de la transferencia. A menudo, JavaScript se comprime cuando se envía a través de la red, lo que significa que la cantidad real de JavaScript es bastante mayor después de que el navegador lo descomprime. Es importante señalar esto, ya que, en lo que respecta al procesamiento de recursos, la compresión es irrelevante. 900 KB de JavaScript sin comprimir siguen siendo 900 KB para el analizador y el compilador, aunque pueden ser alrededor de 300 KB cuando se comprimen.
JavaScript es un recurso costoso de procesar. A diferencia de las imágenes, que solo incurren en un tiempo de decodificación relativamente trivial una vez que se descargan, JavaScript debe analizarse, compilarse y, finalmente, ejecutarse. Byte por byte, esto hace que JavaScript sea más costoso que otros tipos de recursos.
Si bien se realizan mejoras continuamente para mejorar la eficiencia de los motores de JavaScript, mejorar el rendimiento de JavaScript es, como siempre, una tarea para los desarrolladores.
Para ello, existen técnicas que permiten mejorar el rendimiento de JavaScript. La división de código es una técnica de ese tipo que mejora el rendimiento, ya que particiona el JavaScript de la aplicación en fragmentos y entrega esos fragmentos solo a las rutas de una aplicación que los necesita.
Si bien esta técnica funciona, no soluciona un problema común de las aplicaciones con mucho contenido de JavaScript, que es la inclusión de código que nunca se usa. El movimiento de los árboles intenta resolver este problema.
¿Qué es la eliminación de código no utilizado?
La eliminación de código muerto es una forma de eliminar el código no alcanzado. Rollup popularizó el término, pero el concepto de eliminación de código muerto existe desde hace algún tiempo. El concepto también se implementó en webpack, que se muestra en este artículo a través de una app de ejemplo.
El término "tree shaking" proviene del modelo mental de tu aplicación y sus dependencias como una estructura en forma de árbol. Cada nodo del árbol representa una dependencia que proporciona una funcionalidad distinta para tu app. En las apps modernas, estas dependencias se incorporan a través de sentencias import
estáticas de la siguiente manera:
// Import all the array utilities!
import arrayUtils from "array-utils";
Cuando una app es joven (un retoño, si quieres), puede tener pocas dependencias. También usa la mayoría de las dependencias que agregas, si no todas. Sin embargo, a medida que tu app madure, se pueden agregar más dependencias. Para combinar cuestiones, las dependencias más antiguas quedan obsoletas, pero es posible que no se reduzcan de tu base de código. El resultado final es que una app termina publicándose con mucho código JavaScript no utilizado. El desprendimiento de árboles aborda esto aprovechando la forma en que las sentencias import
estáticas extraen partes específicas de los módulos ES6:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
La diferencia entre este ejemplo de import
y el anterior es que, en lugar de importar todo del módulo "array-utils"
, que podría ser mucho código, en este ejemplo solo se importan partes específicas. En las compilaciones para desarrolladores, esto no cambia nada, ya que se importa todo el módulo de todos modos. En las compilaciones de producción, webpack se puede configurar para “sacudir” las exportaciones de los módulos ES6 que no se importaron de forma explícita, lo que hace que esas compilaciones de producción sean más pequeñas. En esta guía, aprenderás a hacerlo.
Busca oportunidades para sacudir un árbol
A modo de ejemplo, hay disponible una app de una página de muestra que demuestra cómo funciona el movimiento de árboles. Puedes clonarla y seguirla si lo deseas, pero cubriremos todos los pasos del camino juntos en esta guía, por lo que la clonación no es necesaria (a menos que el aprendizaje práctico sea lo tuyo).
La app de ejemplo es una base de datos de pedales de efectos de guitarra que se puede buscar. Si ingresas una consulta, aparecerá una lista de pedales de efectos.
El comportamiento que impulsa esta app se divide en proveedores (es decir, Preact y Emotion) y paquetes de códigos específicos de la app (o "fragmentos", como los llama el paquete web):
Los paquetes de JavaScript que se muestran en la figura anterior son compilaciones de producción, lo que significa que están optimizados a través de la uglificación. 21.1 KB para un paquete específico de la app no es malo, pero se debe tener en cuenta que no se está realizando ningún movimiento de árbol. Veamos el código de la app y averigüemos qué se puede hacer para solucionarlo.
En cualquier aplicación, la búsqueda de oportunidades de eliminación de código implican la búsqueda de sentencias import
estáticas. Cerca de la parte superior del archivo de componente principal, verás una línea como la siguiente:
import * as utils from "../../utils/utils";
Puedes importar módulos ES6 de varias maneras, pero los siguientes deberían llamar tu atención. Esta línea específica dice "import
todo del módulo utils
y lo coloca en un espacio de nombres llamado utils
". La gran pregunta que debemos hacernos aquí es: "¿Cuánta cosa hay en ese módulo?"
Si observas el código fuente del módulo utils
, verás que hay alrededor de 1,300 líneas de código.
¿Necesitas todo eso? Para verificarlo, busquemos el archivo de componentes principal que importa el módulo utils
para ver cuántas instancias de ese espacio de nombres aparecen.
Resulta que el espacio de nombres utils
aparece solo en tres lugares de nuestra aplicación, pero ¿para qué funciones? Si vuelves a mirar el archivo del componente principal, parece que solo hay una función, que es utils.simpleSort
, que se usa para ordenar la lista de resultados de la búsqueda según varios criterios cuando se cambian los menús desplegables de ordenamiento:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
De un archivo de 1,300 líneas con muchas exportaciones, solo se usa una. Esto genera el envío de mucho código JavaScript sin usar.
Si bien esta app de ejemplo es un poco artificial, no cambia el hecho de que este tipo de situación sintética se asemeja a las oportunidades de optimización reales que puedes encontrar en una app web de producción. Ahora que identificaste una oportunidad para que el movimiento de árboles sea útil, ¿cómo se hace en realidad?
Evita que Babel transpile módulos ES6 a módulos CommonJS.
Babel es una herramienta indispensable, pero puede hacer que los efectos del movimiento de los árboles sean un poco más difíciles de observar. Si usas @babel/preset-env
, Babel puede transformar los módulos ES6 en módulos CommonJS más compatibles, es decir, módulos que require
en lugar de import
.
Debido a que la eliminación de código no utilizado en los módulos CommonJS es más difícil, webpack no sabrá qué reducir de los paquetes si decides usarlos. La solución es configurar @babel/preset-env
para que deje en paz los módulos ES6 de forma explícita. Dondequiera que configures Babel, ya sea en babel.config.js
o package.json
, esto implica agregar algo más:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Especificar modules: false
en tu configuración de @babel/preset-env
hace que Babel se comporte como se desee, lo que permite que webpack analice tu árbol de dependencias y elimine las dependencias que no se usan.
Ten en cuenta los efectos secundarios
Otro aspecto que debes tener en cuenta cuando sacudes las dependencias de tu app es si los módulos de tu proyecto tienen efectos secundarios. Un ejemplo de un efecto secundario es cuando una función modifica algo fuera de su propio alcance, que es un efecto secundario de su ejecución:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
En este ejemplo, addFruit
produce un efecto secundario cuando modifica el array fruits
, que está fuera de su alcance.
Los efectos secundarios también se aplican a los módulos ES6 y son importantes en el contexto de la eliminación de código no utilizado. Los módulos que toman entradas predecibles y producen resultados igualmente predecibles sin modificar nada fuera de su propio alcance son dependencias que se pueden descartar de forma segura si no las usamos. Son piezas de código modulares y autónomas. Por lo tanto, "módulos".
En lo que respecta a webpack, se puede usar una sugerencia para especificar que un paquete y sus dependencias no tengan efectos secundarios especificando "sideEffects": false
en el archivo package.json
de un proyecto:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
Como alternativa, puedes indicarle a webpack qué archivos específicos no están libres de efectos secundarios:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
En el último ejemplo, se considerará que cualquier archivo que no se especifique no tiene efectos secundarios. Si no quieres agregar esto a tu archivo package.json
, también puedes especificar esta marca en la configuración de webpack a través de module.rules
.
Importa solo lo necesario
Después de indicarle a Babel que deje en paz los módulos ES6, se requiere un ligero ajuste en nuestra sintaxis import
para incluir solo las funciones necesarias del módulo utils
. En el ejemplo de esta guía, todo lo que se necesita es la función simpleSort
:
import { simpleSort } from "../../utils/utils";
Debido a que solo se importa simpleSort
en lugar de todo el módulo utils
, se deberá cambiar cada instancia de utils.simpleSort
a simpleSort
:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
Esto debería ser todo lo que se necesita para que el movimiento de árboles funcione en este ejemplo. Este es el resultado de webpack antes de agitar el árbol de dependencias:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
Este es el resultado después de que el movimiento de árboles se realiza correctamente:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
Si bien ambos paquetes se reducen, es el paquete main
el que más se beneficia. Al sacudir las partes que no se usan del módulo utils
, el paquete main
se reduce alrededor de un 60%. Esto no solo reduce la cantidad de tiempo que tarda la secuencia de comandos en descargarse, sino también el tiempo de procesamiento.
¡Ve a sacudir algunos árboles!
El rendimiento que obtengas del movimiento de árboles depende de tu app, sus dependencias y su arquitectura. Pruébalo Si sabes de verdad que no configuraste el agrupador de módulos para que realice esta optimización, no hay problema en intentar y ver cómo beneficia a tu aplicación.
Es posible que obtengas un aumento significativo del rendimiento con el movimiento de árboles o que no obtengas mucho. Sin embargo, si configuras tu sistema de compilación para aprovechar esta optimización en las compilaciones de producción y, además, importas de forma selectiva solo lo que tu aplicación necesita, mantendrás proactivamente los paquetes de aplicaciones lo más pequeños posible.
Agradecemos especialmente a Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone y Philip Walton por sus valiosos comentarios, que mejoraron significativamente la calidad de este artículo.