Las aplicaciones web actuales pueden ser bastante grandes, en especial la parte de JavaScript. A mediados de 2018, HTTP Archive estimó que el tamaño de transferencia promedio de JavaScript en dispositivos móviles era de aproximadamente 350 KB. ¡Y eso es solo el tamaño de transferencia! JavaScript suele comprimirse cuando se envía a través de la red, lo que significa que la cantidad real de JavaScript es mucho 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 descomprimido siguen siendo 900 KB para el analizador y el compilador, aunque pueden ser aproximadamente 300 KB cuando se comprimen.
JavaScript es un recurso costoso de procesar. A diferencia de las imágenes, que solo generan un tiempo de decodificación relativamente trivial una vez descargadas, JavaScript debe analizarse, compilarse y, luego, ejecutarse. Byte por byte, esto hace que JavaScript sea más costoso que otros tipos de recursos.
Si bien se realizan mejoras continuas 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 para mejorar el rendimiento de JavaScript. La división de código es una de esas técnicas que mejora el rendimiento mediante la partición de JavaScript de la aplicación en fragmentos y la entrega de esos fragmentos solo a las rutas de una aplicación que los necesiten.
Si bien esta técnica funciona, no aborda un problema común de las aplicaciones con mucho JavaScript, que es la inclusión de código que nunca se usa. La eliminación de código no utilizado intenta resolver este problema.
¿Qué es la eliminación de código no utilizado?
El tree shaking es una forma de eliminación de código muerto. El término fue popularizado por Rollup, pero el concepto de eliminación de código muerto existe desde hace un tiempo. El concepto también se encontró en webpack, que se demuestra en este artículo a través de una app de ejemplo.
El término "eliminación de código no utilizado" proviene del modelo mental de tu aplicación y sus dependencias como una estructura similar a un á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 instrucciones import estáticas de la siguiente manera:
// Import all the array utilities!
import arrayUtils from "array-utils";
Cuando una app es joven (un árbol joven, si quieres), puede tener pocas dependencias. También usa la mayoría, si no todas, las dependencias que agregas. Sin embargo, a medida que tu app madura, se pueden agregar más dependencias. Para empeorar las cosas, las dependencias más antiguas dejan de usarse, pero es posible que no se quiten de tu base de código. El resultado final es que una app termina enviándose con mucho JavaScript sin usar. La eliminación de código no utilizado aborda esto aprovechando la forma en que las sentencias import estáticas incorporan 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 import ejemplo y el anterior es que, en lugar de importar todo desde el módulo "array-utils" (que podría ser mucho código), este ejemplo importa solo partes específicas de él. En las compilaciones de desarrollo, 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.
Cómo encontrar oportunidades para sacudir un árbol
A modo ilustrativo, se encuentra disponible una app de ejemplo de una página que demuestra cómo funciona la eliminación de código no utilizado. Puedes clonarla y seguirla si quieres, pero abordaremos cada paso de la guía, por lo que no es necesario clonarla (a menos que te guste el aprendizaje práctico).
La app de ejemplo es una base de datos con capacidad de búsqueda de pedales de efectos de guitarra. Ingresa una consulta y aparecerá una lista de pedales de efectos.
El comportamiento que impulsa esta app se separa en paquetes de código específicos del proveedor (es decir, Preact y Emotion) y de la app (o "fragmentos", como los llama webpack):
Los paquetes de JavaScript que se muestran en la figura anterior son compilaciones de producción, lo que significa que se optimizan a través de la ofuscación. 21.1 KB para un paquete específico de la app no está mal, pero se debe tener en cuenta que no se produce ninguna eliminación de código no utilizado. Veamos el código de la app y qué se puede hacer para solucionar ese problema.
En cualquier aplicación, encontrar oportunidades de tree shaking implicará buscar instrucciones import estáticas. Cerca de la parte superior del archivo del componente principal, verás una línea como esta:
import * as utils from "../../utils/utils";
Puedes importar módulos ES6 de varias maneras, pero los que son como este deberían llamar tu atención. Esta línea específica dice "import todo desde el módulo utils y colócalo en un espacio de nombres llamado utils". La gran pregunta que se debe hacer aquí es "¿cuántas cosas hay en ese módulo?".
Si observas el código fuente del módulo the utils, verás que hay aproximadamente 1,300 líneas de código.
¿ Necesitas todo eso? Para volver a verificar, busca el archivo del componente principal que importa el módulo utils para ver cuántas instancias de ese espacio de nombres aparecen.
utils desde el que importamos toneladas de módulos solo se invoca tres veces en el archivo del componente principal.
Resulta que el espacio de nombres utils aparece solo en tres lugares de nuestra aplicación, pero ¿para qué funciones? Si vuelves a observar el archivo del componente principal, parece ser solo una función, que es utils.simpleSort, que se usa para ordenar la lista de resultados de la búsqueda según una serie de 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 un montón de exportaciones, solo se usa una. Esto genera el envío de mucho 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 la eliminación de código no utilizado sea útil, ¿cómo se hace realmente?
Cómo evitar que Babel transpile módulos ES6 a módulos CommonJS
Babel es una herramienta indispensable, pero puede dificultar un poco más la observación de los efectos de la eliminación de código no utilizado. 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 es más difícil de hacer para los módulos CommonJS, webpack no sabrá qué quitar de los paquetes si decides usarlos. La solución es configurar @babel/preset-env para que deje explícitamente los módulos ES6. 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
}
]
]
}
Si especificas modules: false en tu configuración de @babel/preset-env, Babel se comportará como deseas, lo que permite que webpack analice tu árbol de dependencias y quite las dependencias sin usar.
Ten en cuenta los efectos secundarios
Otro aspecto que debes tener en cuenta cuando quitas dependencias de tu app es si los módulos de tu proyecto tienen efectos secundarios. Un ejemplo de 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 eso importa en el contexto de la eliminación de código no utilizado. Los módulos que toman entradas predecibles y producen salidas igualmente predecibles sin modificar nada fuera de su propio alcance son dependencias que se pueden quitar de forma segura si no las usamos. Son fragmentos de código modulares y autónomos. 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 tienen efectos secundarios. Para ello, se debe especificar "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 tienen efectos secundarios:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
En el último ejemplo, se supondrá que cualquier archivo que no se especifique no tiene efectos secundarios. Si no deseas agregar esto a tu package.json archivo, también puedes especificar esta marca en tu configuración de webpack a través de module.rules.
Importa solo lo que necesitas
Después de indicarle a Babel que deje los módulos ES6, se requiere un pequeño ajuste en nuestra sintaxis import para incorporar 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, cada instancia de utils.simpleSort deberá cambiarse 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 la eliminación de código no utilizado funcione en este ejemplo. Este es el resultado de webpack antes de sacudir 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 la eliminación de código no utilizado 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 redujeron, el paquete main es el que más se beneficia. Al quitar las partes sin usar del módulo utils, el paquete main se reduce en aproximadamente 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.
¡Sacude algunos árboles!
El rendimiento que obtengas del tree shaking dependerá de tu app, sus dependencias y su arquitectura. Pruébalo Si sabes con certeza que no configuraste tu empaquetador de módulos para realizar esta optimización, no hay ningún problema en intentarlo y ver cómo beneficia a tu aplicación.
Es posible que obtengas una mejora significativa en el rendimiento de la eliminación de código no utilizado o no mucho. Sin embargo, si configuras tu sistema de compilación para aprovechar esta optimización en las compilaciones de producción y, de forma selectiva, importas solo lo que necesita tu aplicación, mantendrás de forma proactiva los paquetes de tu aplicación 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.