JavaScript Responsable: Parte II

Jesús Ricarte
A List Apart en Español
12 min readOct 13, 2020

Por Jeremy Wagner. Original en inglés, traducido al español por Jesús Ricarte.

Tanto tú, como el resto del equipo de desarrollo, presionaron con entusiasmo por una re-arquitectura total de la antigua web de la empresa. Sus súplicas fueron escuchadas por la gerencia — incluso por los altos ejecutivos— que les dieron luz verde. Eufóricos, comenzaron a trabajar con los equipos de diseño y contenido. En poco tiempo, estaban lanzando código nuevo.

Comenzó de manera bastante inocente con un npm install por aquí y un npm install por allá. Sin embargo, antes de que te dieses cuenta, estabas instalando dependencias de producción como si no hubiese un mañana.

Y lanzaste la nueva web al público.

A diferencia de la mayoría de borracheras, la agonía no empezó a la mañana siguiente. Oh, no. Llegó meses después, en forma de náuseas leves y dolor de cabeza de los responsables de producto y mandos intermedios, preguntándose por qué las conversiones y los ingresos han disminuido desde el lanzamiento. Luego llegó al punto álgido cuando el CTO regresó de un fin de semana en una cabaña, preguntándose por qué la web cargaba tan lento en su teléfono — si de hecho alguna vez llegó a cargar.

Todos estaban felices. Ahora nadie está feliz. Bienvenido a tu primera resaca de JavaScript.

No es culpa tuya

Cuando estás lidiando con una cruel resaca, “te lo dije” sería una reprimenda bien merecida, aunque pueda provocar alguna pelea — asumiendo que pudieses luchar en un estado tan lamentable.

Cuando se trata de resacas de Javascript, hay mucha culpa que repartir. Sin embargo, señalar con el dedo es una pérdida de tiempo. El panorama web actual exige que iteremos más rápido que nuestros competidores. Este tipo de presión significa que es probable que aprovechemos cualquier medio disponible para ser lo más productivos posible. Eso significa que tendremos más probabilidades, aunque no estemos necesariamente condenados — de crear aplicaciones con una sobrecarga mayor, y posiblemente utilizar patrones que puedan afectar al rendimiento y a la accesibilidad.

El desarrollo web no es fácil. Es un trabajo prolongado, en el que rara vez acertamos al primer intento. Sin embargo, lo mejor de trabajar en la web, es que no tenemos por qué hacerlo perfecto a la primera. Podemos hacer mejoras a posteriori, y justo para eso está aquí la segunda entrega de esta serie. La perfección queda muy lejos. Por ahora, vamos a aliviar esa resaca de JavaScript mejorando la situación del código de tu web, a corto plazo.

Reúne a los sospechosos habituales

Puede parecer rutinario, pero vale la pena revisar la lista de optimizaciones básicas. No es extraño que equipos de desarrollo extensos — particularmente aquellos que trabajan con varios repositorios o no usan plantillas optimizadas — los pasen por alto.

Sacude el árbol

Primero, asegúrate que tu cadena de herramientas esté configurada para hacer tree shaking. Si es algo nuevo para ti, escribí una guía el año pasado, que puedes consultar. En resumen, tree shaking es un proceso por el que las exportaciones no utilizadas en tu código no se añadirán a tus paquetes en producción.

Three shaking está listo para usar con empaquetadores de código modernos como webpack, Rollup, o Parcel. Grunt o gulp — que no son empaquetadores, si no ejecutores de tareas — no se ocuparán de esto por ti. Un ejecutor de tareas no crea un gráfico de dependencias, tal como hace un empaquetador. Más bien, realiza tareas independientes en los archivos que se le proporcionan. Los ejecutores de tareas se pueden extender para utilizar empaquetadores para procesar Javascript. Si extender un ejecutor de tareas de esta manera te resulta problemático, es probable que debas auditar y eliminar manualmente el código no utilizado.

Para que el tree shaking sea efectivo, se deben cumplir los siguientes puntos:

  1. La lógica de tu aplicación, así como los paquetes que instales en tu proyecto, deben ser creados como módulos ES6. No es posible hacer tree shaking de módulos CommonJS.
  2. Tu empaquetador no debe transformar los módulos ES6 en otro formato de módulo durante el tiempo de compilación. Si esto sucede en una cadena de herramientas que usa Babel, la configuración @babel/preset-env debe especificar modules: false para prevenir que el código ES6 sea convertido a CommonJS.

En caso de que no se realize tree shaking durante la compilación, hacer que funcione puede resultar de ayuda. Por supuesto, su efectividad varía según el caso. También depende de si los módulos que importas introducen efectos secundarios, lo que puede influir en la capacidad de un empaquetador para deshacerse de las exportaciones de código no utilizadas.

Divide el código

Es muy probable que estés empleando alguna forma de división de código, pero vale la pena re-evaluar cómo lo estás haciendo. Sin importar cómo estés dividiendo el código, hay dos preguntas que siempre vale la pena que te hagas:

  1. ¿Estás des-duplicando código en común entre puntos de entrada?
  2. ¿Estás postergando la carga de toda la funcionalidad posible con import() dinámicos?

Esto es importante porque reducir el código redundante es esencial para el rendimiento. Postergar la carga de funcionalidad también mejora el rendimiento al reducir la huella de JavaScript inicial en una página determinada. En lo que respecta a la redundancia, el uso de una herramienta de análisis como Bundle Buddy puede ayudarte a averiguar si tienes un problema.

Bundle Buddy puede examinar tus estadísticas de compilación de webpack y determinar cuanto código se comparte entre tus paquetes.

En lo que respecta a la carga diferida, puede resultar un poco difícil saber dónde empezar a buscar oportunidades de mejora. Cuando busco dichas oportunidades en proyectos ya existentes, buscaré puntos de interacción del usuario en todo el código, como eventos de clic, eventos de teclado, y candidatos similares. Cualquier código que requiere una interacción del usuario para ejecutarse es un buen candidato potencial para un import() dinámico.

Por supuesto, la carga de código bajo demanda conlleva la posibilidad de que la interactividad se retrase notablemente, ya que el código necesario para la interacción se debe descargar primero. Si el uso de datos no es un problema, considera usar el indicador rel=prefetch, para cargar estos códigos con prioridad baja, por lo que no competirán por el ancho de banda contra otros recursos críticos. Aunque el soporte de rel=prefetch es bueno, nada se romperá si no es compatible, ya que los navegadores ignorarán el código que no entiendan.

Externaliza el código de terceros

Idealmente, deberías alojar tantas dependencias de tu web como sean posibles. Si por cualquier motivo debes cargar dependencias de terceros, márcalas como externas en la configuración de tu empaquetador. No hacerlo podría significar que los visitantes de tu web descarguen tanto el código alojado localmente como el mismo código alojado por un tercero.

Veamos una situación hipotética en la que esto podría perjudicarte: pongamos que tu web carga Lodash desde una CDN pública. Además, has instalado Lodash en tu proyecto, para el desarrollo en local. Sin embargo, si no marcas Lodash como externo, tu código de producción terminará cargando una copia de terceros además de la copia alojada localmente.

Esto puede parecer de sentido común si sabes cómo funcionan los empaquetadores, pero he visto que se suele pasar por alto. Vale la pena comprobarlo dos veces.

Si no te convence alojar tus dependencias de terceros, puedes considerar añadir los indicadores dns-prefetch, preconnect, o incluso preload. Si lo haces, puedes reducir el tiempo para que tu web sea interactiva y — si Javascript es fundamental para renderizar el contenido — el índice de velocidad de tu web.

Alternativas más pequeñas para una sobrecarga menor

El ecosistema Javascript es como una tienda de golosinas obscenamente enorme, y a los programadores nos encanta el gran volumen de ofertas de código abierto. Los frameworks y librerías nos permiten ampliar nuestras aplicaciones, para hacer rápidamente todo tipo de cosas que de otro modo requerirían mucho tiempo y esfuerzo.

Aunque personalmente prefiero minimizar, de manera agresiva, el uso de frameworks y librerías, del lado de cliente, en mis proyectos, su valor es irresistible. Sin embargo, tenemos la responsabilidad de ser un poco restrictivos cuando se trata de lo que instalamos. Cuando hemos construido y publicado algo que depende de gran cantidad de código instalado para ejecutarse, hemos aceptado un costo de base que solo los mantenedores de ese código pueden abordar, ¿verdad?.

Quizás sí, o quizás no. Depende de las dependencias utilizadas. Por ejemplo, React es extremadamente popular, pero Preact es una alternativa ultra-pequeña que comparte en gran medida la misma API y conserva la compatibilidad con muchos complementos de React. Luxon y date-fns son alternativas mucho más compactas que moment.js, que no es precisamente pequeño.

Librerías como Lodash ofrecen muchos métodos útiles. Sin embargo, algunos de ellos se pueden reemplazar fácilmente con ES6 nativo. El método compact de Lodash, por ejemplo, es reemplazable por el método de colecciones filter. Muchos otros se pueden reemplazar sin mucho esfuerzo, y sin la necesidad de cargar una gran librería de utilidades.

Sean cuales sean tus herramientas preferidas, la idea es la misma: investiga un poco para ver si hay alternativas más pequeñas, o si las características nativas del lenguaje te pueden servir. Te puedes sorprender del poco esfuerzo que puede requerir reducir seriamente la sobrecarga de tu aplicación.

Sirve tu código de manera diferencial

Es muy probable que estés utilizando Babel en tu cadena de herramientas para transformar tu código ES6 en un código que pueda ejecutarse en navegadores más antiguos. ¿Significa esto que estamos condenados a ofrecer paquetes gigantes incluso a navegadores que no los necesitan, hasta que los navegadores más antiguos desaparezcan por completo? ¡Por supuesto que no! La entrega diferencial nos ayuda a solucionarlo al generar dos compilaciones diferentes de tu código ES6:

  • El paquete uno, que contiene todas las transformaciones y polyfills necesarios para que tu web funcione en navegadores antiguos. Probablemente ya estés sirviendo este paquete en este momento.
  • El paquete dos, que contiene pocas o ninguna de las transformaciones y polyfills, porque se dirige a los navegadores modernos. Este es el paquete que probablemente no estés sirviendo — al menos no todavía.

Lograr esto es un poco complicado. Escribí una guía sobre una forma de hacerlo, por lo que no es necesario profundizar en ello aquí. En resumen, puedes modificar la configuración de tu compilación para generar una versión adicional, pero más pequeña, del código JavaScript de tu web, y servirlo sólo a navegadores modernos. La mejor parte es que este ahorro se puede lograr sin sacrificar ninguna característica o funcionalidad que ya estés ofreciendo. Dependiendo del código tu aplicación, el ahorro podría ser bastante significativo.

Un análisis de webpack-bundle-analyzer de un paquete heredado antiguo (izquierda) frente a un paquete moderno (derecha). Ver la imagen a tamaño completo.

El patrón más simple para servir estos paquetes a sus respectivas plataformas es breve. Además funciona de maravilla en los navegadores modernos:

<!-- Los navegadores modernos cargarán este archivo: -->
/js/app.mjs
<!-- Los navegadores antiguos cargarán este archivo: -->
/js/app.js

Desafortunadamente, hay una salvedad con este patrón: navegadores antiguos como IE 11 — e incluso los relativamente modernos, como Edge en las versiones 15 a 18 — descargarán ambos paquetes. Si esto te parece una contrapartida aceptable, no te preocupes más por ello.

Por otro lado, necesitarás una solución alternativa si te preocupan las implicaciones en el rendimiento de los navegadores más antiguos que descargan ambos conjuntos de paquetes. Esta es una posible solución que utiliza la inyección de código (en lugar de las etiquetas script anteriores) para evitar descargas dobles en los navegadores afectados:

var scriptEl = document.createElement(“script”);if (“noModule” in scriptEl) {
//
Configura el script moderno
scriptEl.src = “/js/app.mjs”;
scriptEl.type = “module”;
} else {
//
Configura el script antiguo
scriptEl.src = “/js/app.js”;
scriptEl.defer = true; // type=”module” defers by default, so set it here.
}
// Inyecta!
document.body.appendChild(scriptEl);

Este código infiere que si un navegador admite el atributo nomodule en elemento script, también entenderá type = “module”. Esto garantiza que los navegadores antiguos sólo obtengan los scripts antiguos y que los navegadores modernos sólo obtengan los modernos. Ten en cuenta, sin embargo, que los scripts inyectados dinámicamente se cargan, por defecto, de forma asincrónica, así que establece el atributo async a false si el orden de dependencia es crucial.

Transpila menos

No estoy aquí para desprestigiar a Babel. Es indispensable, pero agrega un montón de cosas adicionales sin que te des cuenta. Vale la pena echar un vistazo debajo del capó para ver lo que está haciendo. Algunos cambios menores en tus hábitos de programación pueden tener un impacto positivo en el código que genera Babel.

https://twitter.com/_developit/status/1110229993999777793

A saber: los parámetros predeterminados son una característica muy útil de ES6, que probablemente ya estés usando:

function logger(message, level = "log") {
console[level](message);
}

A lo que hay que prestar atención aquí es al parámetro level, que tiene un valor predeterminado “log”. Esto significa que si queremos invocar console.log con esta función contenedora, no necesitamos especificar level. Genial, ¿verdad? Excepto cuando Babel transforma esta función, cuya salida sería así:

function logger(message) {
var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";

console[level](message);
}

Este es un ejemplo de cómo, a pesar de nuestras mejores intenciones, las comodidades para los desarrolladores pueden ser contraproducentes. Lo que era un puñado de bytes en nuestro código fuente, se ha transformado en algo mucho más grande en nuestro código de producción. La uglificación tampoco puede hacer mucho al respecto, ya que los argumentos no se pueden reducir. Ah! y si crees que los parámetros Rest pueden ser un buen antídoto, las trasnformaciones que genera Babel son aún más voluminosas:

// Código fuente
function logger(...args) {
const [level, message] = args;
console[level](message);
}
// Código generado por Babel
function logger() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
const level = args[0],
message = args[1];
console[level](message);
}

Peor aún, Babel transforma este código incluso en proyectos con una configuración @babel/preset-env dirigida a navegadores modernos, lo que significa que ¡los paquetes modernos de su JavaScript servido diferencialmente también se verán afectados! Podrías utilizar transformaciones loose para suavizar el golpe — y es una buena idea, ya que a menudo son un poco más pequeñas que sus contrapartes que cumplen con las especificaciones — pero habilitar las transformaciones loose puede causar problemas si, más adelante, eliminas Babel de tu cadena de compilación.

Independientemente de si decides habilitar las transformaciones loose, aquí tienes una forma de eliminar el problema de los parámetros predeterminados transpilados:

// Babel no modificará este código
function logger(message, level) {
console[level || "log"](message);
}

Por supuesto, los parámetros predeterminados no son la única característica con la que hay que tener cuidado. Por ejemplo, la sintaxis extendida se transforma, al igual que las funciones flecha y una gran cantidad de otras cosas.

Si no deseas evitar estas funciones por completo, tienes un par de maneras de reducir su impacto:

  1. Si estás creando una librería, considera usar @babel/runtime junto con @babel/plugin-transform-runtime para des-duplicar las funciones auxiliares que Babel añade a tu código.
  2. Para las funciones polyfill, puedes incluirlas de forma selectiva con @babel/polyfill a través de la opción useBuiltIns: “usage” de @babel/preset-env.

Esta es tan sólo mi opinión, pero creo que la mejor opción es evitar por completo la transpilación en paquetes generados para navegadores modernos. Algo que no siempre es posible, especialmente si usas JSX, que debe transformarse para todos los navegadores, o si usas características del lenguaje tan de vanguardia, que no son ampliamente compatibles. En este último caso, podría valer la pena preguntarse si esas características son realmente necesarias para ofrecer una buena experiencia de usuario (rara vez lo son). Si llegas a la conclusión de que Babel debe ser parte de tu cadena de herramientas, vale la pena echar un vistazo bajo el capó, de vez en cuando, para detectar cosas que Babel podría estar haciendo de manera no óptima y que puedes mejorar.

La mejora no es una carrera

Mientras te masajeas las sienes, preguntándote cuándo va a desaparecer esta horrible resaca de JavaScript, comprende que, precisamente, cuando nos apresuramos a sacar algo lo más rápido posible, es cuando la experiencia del usuario puede resentirse. A medida que la comunidad de desarrollo web se obsesiona con iterar más rápido, en nombre de la competencia, vale la pena que ralentices un poco el paso. Descubrirás que al hacerlo, posiblemente no estés iterando tan rápido como tus competidores, pero tu producto será más rápido que el suyo.

A medida que tomas estas sugerencias y las aplicas a tu código, sé consciente de que el progreso no ocurre espontáneamente de la noche a la mañana. El desarrollo web es un trabajo. El trabajo que causa verdadero impacto se realiza cuando somos reflexivos y dedicados al oficio a largo plazo. Concéntrate en la mejora constante. Mide, prueba y repite, la experiencia de usuario de tu web mejorará, y conseguirás ser más rápido, poco a poco, con el tiempo.

Un agradecimiento especial a Jason Miller por la edición técnica de esta pieza. Jason es el creador y uno de los muchos mantenedores de Preact, una alternativa mucho más pequeña a React, con la misma API. Si usas Preact, considera apoyar Preact a través de Open Collective.

Si te gusta lo que hace A List Apart, ¡haz una inversión en apoyarnos! O síguenos en Twitter y Facebook. ¡Gracias!

--

--

Jesús Ricarte
A List Apart en Español

Front-end developer: UX, performance, clean code, cross-browser, responsive. @alistapartES volunteer translator. I like neither drop-downs nor magic numbers.