Factorizando bundles de JavaScript agrupando dependencias comunes

Durante el pasaje a producción de una single-page application (SPA) es muy importante el proceso de bundling, que consiste en combinar los archivos y módulos de la aplicación para su puesta en marcha.

En el panel de Mango utilizamos Browserify para estructurar el Front-end.
Browserify nos proporciona muchas ventajas y utilidades como contamos en este post.
El proceso de bundling más simple utilizando browserify resulta en un único archivo de JavaScript que contiene toda la lógica de la aplicación.

El problema del bundle único

A medida que se agregan nuevas features el bundle resultante incrementa en peso acarreando dos inconvenientes:


– El tiempo de descarga es mayor.
– El tiempo de interpretado del código es mayor.

El panel, como la mayoría de las SPA, necesita descargar, parsear e interpretar el código antes de empezar a dibujar. Esto se traduce en tiempos más largos de espera antes de poder utilizar la aplicación, afectando a la experiencia del usuario.

El primer problema, si bien es real, no se manifiesta de modo lineal a medida que crece la cantidad de código resultante. Como apunta TJ VanToll la compresión gzip es muy fuerte y funciona mejor para archivos más grandes (mientras más grande el archivo hay más repeticiones por encontrar).

El inconveniente en materia de performance más grande está relacionado con el tiempo de parsing e interpretado del código inicial.

Filament group hizo un análisis interesante sobre la penalización en la utilización de ciertos frameworks MVC populares en materia de carga inicial que da un buen panorama del problema.

factor-bundle al rescate

factor-bundle funciona como un plugin de browserify que recibe múltiples archivos como puntos de entrada (en nuestro caso están mapeados a las secciones de nuestra aplicación), analiza sus contenidos y genera por un lado un archivo de salida por cada archivo de entrada sumado a un archivo extra que contiene las dependencias en común entre todos ellos.

Gracias a esta ‘factorización’ de dependencias, cuando un usuario entra a la app solo descarga el JavaScript asociado a la sección que visita, sumado a las dependencias compartidas. El resto de las secciones se descargan de modo asincrónico y sin repetir la descarga de dependencias, mientras el usuario navega por el panel.

Una pequeña optimización que hacemos es producir, por cada archivo de salida, otro que surge de concatenarle a este el archivo de dependencias comunes. Esto previene la descarga de un script adicional (-common-.js).

El output luego del procesado se visualiza de este modo:

.
|-- config-0.3.6.js
|-- config-common-0.3.6.js
|-- login-0.3.6.js
|-- login-common-0.3.6.js
|-- store-single-0.3.6.js
|-- store-single-common-0.3.6.js
|-- stores-0.3.6.js
|-- stores-common-0.3.6.js
|-- user-0.3.6.js
`-- user-common-0.3.6.js

Existen optimizaciones para aplazar la carga de módulos no usados. Por ejemplo detectar el acercamiento del mouse a links de otras secciones. Decidimos dejar esas optimizaciones para más adelante (y para otros posts).

Conclusiones

La técnica de separación de bundles resultó en una mejora de aproximadamente un 30% de ahorro en el tiempo de espera promedio de los usuarios a la hora de ingresar a nuestro Panel de administración, dado por la baja en el peso del script inicial como la menor cantidad de código a interpetar. Además, el agregado de nuevas secciones a la aplicación no resulta en una reducción de la performance en la carga inicial del sistema.

Hay otras técnicas para mejorar la carga inicial de SPAs. Una de las que estamos investigando para integrar es la implementación de rendering compartido entre el cliente y el servidor. Una ventaja interesante de la factorización de bundles es que es un cambio casi exclusivamente del proceso de compilación de la aplicación. La lógica de negocio permaneció intacta.

Cualquier duda/comentario o experiencias que quieran compartir, me pueden contactar vía Twitter a @dzajdband.

Los comentarios están cerrados para este artículo.