Logo Studenta

Un pluggin para reducir el tamano de archivos Java Script en aplicaciones web

¡Este material tiene más páginas!

Vista previa del material en texto

UNIVERSIDAD NACIONAL DEL CENTRO DE
LA PROVINCIA DE BUENOS AIRES
FACULTAD DE CIENCIAS EXACTAS
TRABAJO FINAL DE INGENIERÍA DE SISTEMAS
Un pluggin para reducir el tamaño de archivos
JavaScript en aplicaciones web
AUTOR
Leonardo Ceferino Canalejo
DIRECTORES
Dr. Santiago Vidal y Dra. Claudia Marcos
Tandil, Agosto de 2022
Agradecimientos
Quiero agradecer a todas las personas que formaron parte de esta etapa de mi vida,
principalmente a mi familia. En particular, a mi mamá y mis hermanos quienes siempre
confiaron en mí y me brindaron todo su apoyo para llevar adelante mis estudios.
A mi novia, por su constante apoyo y por haberme acompañado durante toda esta etapa.
A mis directores de tesis Dr. Santiago Vidal y Dra. Claudia Marcos por su constante guía y
paciencia, que fueron indispensables en el desarrollo de este trabajo.
A la Universidad Nacional del Centro de la Provincia de Buenos Aires, y en particular a la
Facultad de Ciencias Exactas por la formación allí recibida.
A mis amigos de siempre de Quequén y Tandil, y a los que fui conociendo a lo largo de esta
etapa. A todos mis compañeros de cursada, por los momentos compartidos a lo largo de la
carrera.
A mi viejo, que ya no está, pero que fue muy importante desde el comienzo. Ojalá
estuvieras acá para compartir este momento.
1
Índice general
Agradecimientos 1
Índice general 2
Índice de figuras 5
Capítulo 1: Introducción 8
1.1 Motivación 8
1.2 UFFR Extension 9
1.3 Organización del trabajo final 12
Capítulo 2: Desarrollo de aplicaciones JavaScript 13
2.1 JavaScript como lenguaje de programación 13
2.1.1 Sistemas de módulos 15
2.1.1.1 Global Variables (variables globales) 16
2.1.1.1.1 Anonymous Closures (cierres anónimos) 16
2.1.1.1.2 Global Import (importación global) 17
2.1.1.1.3 Module Export (módulo de exportación) 17
2.1.1.2 Advanced Patterns (patrones avanzados) 18
2.1.1.2.1 Augmentation 18
2.1.1.2.2 Loose Augmentation 18
2.1.1.2.3 Tight Augmentation 19
2.1.1.2.4 Cloning and Inheritance (clonación y herencia) 19
2.1.1.2.5 Sub-modules 20
2.1.1.3 Revealing Module 20
2.1.1.4 CommonJS 21
2.1.1.5 Asynchronous Module Definition (AMD) 23
2.1.1.6 Módulos en ES2015 (ECMAScript 6) 25
2.1.1.7 UMD (Universal Module Definition) 27
2.1.2 Distribución de aplicaciones Javascript 28
2.1.3 Minificación y Bundling 28
2.2 Resumen 29
2
Capítulo 3: Trabajos relacionados 31
3.1 Herramientas relevantes 31
3.1.1 Lazy propagation 31
3.1.2 Modelado del HTML DOM y de la API del navegador 32
3.1.3 JSNOSE 33
3.1.4 Browserify 34
3.1.5 Webpack 35
3.1.6 Rollup 36
3.1.7 UFFRemover 37
3.2 Análisis Comparativo 39
3.3 Resumen 40
Capítulo 4: UFFR Extension 42
Actividades de UFFR Extention 42
4.1 Detección y selección de archivos JavaScript 45
4.2 Instrumentación 46
4.3 Reemplazo de archivos originales por los instrumentados 48
4.3.1 Herramientas para descargar páginas web 49
4.3.1.1 WebHTTrack 49
4.3.1.2 SiteSucker 49
4.3.1.3 GNU Wget 49
4.3.2 Herramientas para redireccionar URLs de recursos web 50
4.3.2.1 Requestly 50
4.3.2.1 Fiddler 52
4.4 Detección de funciones no utilizadas 53
4.5 Optimización 54
4.5.1 Librerías externas utilizadas 57
4.6 Reemplazo de archivos originales por optimizados 58
Capítulo 5: Casos de estudio 59
5.1 Preguntas de investigación 59
5.2 Diseño de las pruebas 59
3
5.2.1 Métricas 62
5.2.2 Preparación 63
5.3 Resultados 65
5.3.1 Análisis contemplando los archivos JavaScript 65
5.3.2 Análisis contemplando todos los tipos de archivo 68
5.3.3 Falsos positivos 72
5.3.4 Relación entre código eliminado y función $dl 73
5.4 Lecciones aprendidas 74
Capítulo 6: Conclusiones 76
6.1 Limitaciones 77
6.2 Trabajos a futuro 77
Bibliografía 78
4
Índice de figuras
Figura 1.1: Diagrama de actividades utilizando extensión de Chrome UFFR Extension 11
Figura 2.1: Cadena de dependencias de Chart.js 15
Figura 2.2: Declaración de una función que se ejecutará inmediatamente 16
Figura 2.3: Funciona anónima con variables globales como parámetros 17
Figura 2.4: Declaración del módulo module 17
Figura 2.5: Augmentation de un module desde arriba 18
Figura 2.6: Estructura de un módulo para aceptar augmentation asíncrono 19
Figura 2.7: Ejemplo de anulación de un método utilizando tight augmentation 19
Figura 2.8: Ejemplo de clonación y herencia 20
Figura 2.9: Ejemplo de definición de submódulo 20
Figura 2.10: Ejemplo del patrón Revealing Module 21
Figura 2.11: Ejemplo de la sintaxis de CommonJS 22
Figura 2.12: Ejemplo de dependencia entre módulos con CommonJS 23
Figura 2.13: Ejemplo de múltiple dependencia entre módulos con CommonJS 23
Figura 2.14: Ejemplo de la sintaxis de AMD 23
Figura 2.15: Ejemplo guia del uso de AMD 24
Figura 2.16: Ejemplo de sintaxis de ES2015 (también conocido como ES6) 26
Figura 2.17: Ejemplo de exportación/importación por defecto en ES2015 26
Figura 2.18: Ejemplos de importaciones en ES2015 27
Figura 2.19: Ejemplo de sintaxis de UMD 27
Figura 2.20: Descripción del proceso de desarrollo de un paquete JavaScript 29
Figura 3.1: Código ejemplo para Lazy propagation 32
Figura 3.2: Vista de procesamiento de JSNOSE, detector de code smells JavaScript 34
Figura 3.3: Uso de Browserify 34
Figura 3.4: Empaquetado con Webpack 35
Figura 3.5: Archivo de ejemplo de main.js 37
Figura 3.6: Archivo de ejemplo foo.js 37
Figura 3.7: Ejemplo de bundle.js usando Rollup 37
Figura 3.8: Resumen del tratamiento de UFF 38
Figura 3.9: Comparación de características de las distintas aplicaciones 39
Figura 4.1: Diagrama de actividades utilizando la herramienta UFFR Extension 43
Figura 4.2: Página web utilizada como ejemplo guía en este capítulo 44
Figura 4.3: Archivo JavaScript ejemplo.js utilizado por la página web del ejemplo guía 45
Figura 4.4: Interfaz visual de la herramienta UFFR Extension 46
Figura 4.5: Función utilizada en el proceso de instrumentación 46
Figura 4.6: Archivo ejemplo.js instrumentado (ejemplo_instr.js) 48
Figura 4.7: Ejemplo de definición de una regla para redirección en Requestly 50
5
Figura 4.8: Tabla con los resultados típicos para las comprobaciones de same-origin
policy contra la URL de ejemplo http://www.example.com/dir/page.html 52
Figura 4.9: Ejemplo de configuración de las redirecciones utilizando Fiddler 53
Figura 4.10: Función listener de UFFR Extension, utilizada para capturar los mensajes 54
Figura 4.11: Archivo profiling/log del ejemplo guia 54
Figura 4.12: Ejemplo de mecanismo de soft-delete 55
Figura 4.13: Función para cargar de manera dinámica funciones eliminadas por
UFFR Extension 55
Figura 4.14: Archivo js1.js optimizado 56
Figura 4.15: Archivo ejemplo-$1.js 57
Figura 5.1: Tabla con las páginas web utilizadas como parte de los experimentos 60
Figura 5.2: Ejemplo de redirección de URLs de archivos JavaScript
instrumentados utilizando Fiddler 61
Figura 5.3: Ejemplo de redirección de URLs de archivos JavaScript optimizados
utilizando Fiddler 62
Figura 5.4: Tamaño total en KB de cada tipo de archivo por página web 63
Figura 5.5: Porcentaje de cada tipo de archivo en base al tamaño por página web 64
Figura 5.6: Promedio del porcentaje de cada tipo de archivo utilizado por las
páginas web 65
Figura 5.7: Tabla comparativa entre archivos JavaScript originales y optimizados 66
Figura 5.8: Gráfico de barras de comparativa de tamaños de archivos JS
originales vs. optimizados 66
Figura 5.9: Factor de compresión (%) de archivos JS logrado luego de la optimización 67
Figura 5.10: Ahorro en tamaño (KB) de archivos optimizados respecto a los originales 67
Figura 5.11: Tamaño total en KB de cada tipo de archivo por página web luego
de la optimización 68
Figura 5.12: Porcentaje de cada tipo de archivo en base al tamaño por página web
luego de la optimización 69
Figura 5.13: Promedio del porcentaje de cada tipo de archivo utilizado por las
páginas web luego de la optimización 70
Figura 5.14: Tabla comparativa entre tamaño total (en KB) de la web original
y luego de optimizar 70
Figura 5.15: Gráfico de barras de comparativa de tamaño total original vs. post
optimización71
Figura 5.16: Factor de compresión (%) total logrado luego de la optimización 71
Figura 5.17: Cantidad de falsos positivos detectados por página web 72
Figura 5.18: Función $dl minificada 73
Figura 5.19: Comparativa entre el tamaño ahorrado y el ocupado por la función $dl 74
6
7
Capítulo 1: Introducción
JavaScript (JS) es uno de los principales lenguajes para el desarrollo de
aplicaciones web, ya que da soporte a la codificación del comportamiento de las mismas [1].
Para que los navegadores puedan renderizar una página web, es necesario descargar a
través de la red los archivos que conforman dicha página, como imágenes, archivos de
estilo (CSS), fuentes, código JavaScript, entre otros. Cuanto más grandes sean estos
archivos, más tiempo tardará el navegador en hacer la descarga y en mostrar las páginas
web al usuario final, afectando de manera negativa el desempeño de la aplicación y por
ende la experiencia de los usuarios que navegan en el sitio. Adicionalmente, en el contexto
de dispositivos móviles, un tamaño más grande de archivo también implica un mayor
consumo de datos y batería [3].
Cuando un usuario accede a una página web es habitual que se descarguen
grandes cantidades de contenido JS. Sin embargo, JavaScript fue concebido inicialmente
sólo para codificar funcionalidades simples que representaban pequeñas partes de código.
En la actualidad se utiliza comúnmente para programar aplicaciones de gran tamaño
mediante la utilización de un gran número de bibliotecas y frameworks JS que se
encuentran disponibles para cumplir muchos de los requerimientos funcionales y no
funcionales. Esto complejiza y aumenta el tamaño de las aplicaciones. Por este motivo, este
trabajo plantea la necesidad de generar una herramienta para la gestión inteligente del
contenido de los archivos JS. El objetivo es optimizar el tamaño de los archivos requeridos
por una aplicación web particular y mejorar su desempeño.
1.1 Motivación
En la actualidad las aplicaciones JavaScript se componen de varios archivos que
suelen poseer un tamaño considerable, lo cual incide directamente en el desempeño de las
páginas web, impactando negativamente en la calidad de las mismas. Esto se debe
principalmente a una serie de problemas que son provocados por el tamaño de los archivos.
Uno de los problemas más importantes que producen los archivos grandes es que afectan
de manera negativa la performance de la aplicación y su usabilidad (UX ). Un ejemplo de1
este problema se evidencia en las aplicaciones web para dispositivos móviles, en la cual
muchos usuarios experimentan una navegación “lenta” y terminan optando por abandonar la
aplicación. La reducción del tamaño de los archivos JavaScript, además de mejorar los
tiempos de carga en el navegador, implica reducir la cantidad de datos para las aplicaciones
distribuidas a través de internet, y, en el caso de dispositivos móviles, disminuir la memoria
requerida y el consumo de energía [3].
La calidad en el desempeño de un sitio web, es una característica diferenciadora
entre las empresas de desarrollo de software. La posibilidad de acelerar la carga de las
1 https://en.wikipedia.org/wiki/User_experience
8
páginas y mejorar la experiencia de usuario aparece entonces como una clara oportunidad
de ventaja competitiva. Un segundo problema es que los archivos JS son cada vez más
grandes, debido, por un lado, a la exigencia de interfaces de usuario modernas y complejas,
y, por otro lado, a la utilización de bibliotecas y frameworks de terceros [2].
Sin embargo, no todo el código JavaScript descargado es necesario para la
ejecución y visualización de una página Web. Por ejemplo, un desarrollador puede haber
importado una biblioteca completa (por ej., jQuery , Angular , chart-js , etc.) y luego solo2 3 4
utilizar algunas de sus funcionalidades, provocando que el navegador descargue código
JavaScript que nunca se va a usar. En estas situaciones, se plantea el desafío de proveer
una herramienta que permita identificar porciones de código que finalmente son requeridas
en el contexto de una aplicación, y eliminar las que no son necesarias.
1.2 UFFR Extension
JavaScript, al ser un lenguaje dinámico, hace que realizar un análisis estático del
código resulte un poco más complejo y muchas veces inexacto, ya que los elementos del
lenguaje tienen "menos" información que otros lenguajes, por ejemplo, con información de
tipos, etc. En este contexto, es importante contar con una herramienta que recopile
información sobre qué partes del código son utilizadas en tiempo de ejecución para luego
decidir cómo optimizar los archivos JavaScript.
Las técnicas y herramientas existentes más conocidas para realizar un proceso de
optimización carecen de mecanismos para asistir a los desarrolladores en la reducción de
código innecesario de acuerdo al uso real que hacen los usuarios de la página. Existen
técnicas, como por ej, Uglify , Webpack , Browserify , que intentan reducir el código en5 6 7
tiempo de desarrollo, quitando funciones que no se han utilizado a nivel de código [7]. Este
análisis se realiza de forma estática ya que revisa el código y busca funciones que no están
referenciadas. Sin embargo, estas técnicas no utilizan información generada dinámicamente
por el uso de las aplicaciones, por lo cual, no detectan código innecesario que tenga que ver
con el uso de la aplicación. Un análisis dinámico de la aplicación se daría obteniendo
información relevante durante la ejecución. Por ejemplo, una función que es llamada
durante el uso de una página web.
Una herramienta que ha sido utilizada para realizar este tipo de análisis en archivos
JS es UFFRemover [6], la misma permite el análisis del uso de las funciones durante el uso8
de una página Web. Para ello UFFRemover obtiene información sobre las ejecuciones de
funciones en tiempo de ejecución para poder eliminar funciones no utilizadas. Sin embargo,
UFFRemover cuenta con algunas limitaciones. Una de ellas es que no es una herramienta
8 https://github.com/hcvazquez/UFFRemover
7 https://www.npmjs.com/package/browserify
6 https://www.npmjs.com/package/webpack
5 https://www.npmjs.com/package/uglify-js
4 https://www.chartjs.org/
3 https://angular.io/
2 https://jquery.com/
9
que presente una buena usabilidad para optimizar páginas web enteras. Otra limitación es
que no tiene una forma nativa de detectar falsos positivos (detectar y eliminar una función
como no utilizada, cuando en realidad sí lo era).
En este contexto, utilizando como base la herramienta UFFRemover, en este trabajo
se presenta una herramienta de asistencia a los desarrolladores que permite identificar y
remover funcionalidad no requerida por aplicaciones web, mediante el análisis dinámico del
uso de la aplicación. La herramienta se implementó como un plugin para el navegador
Google Chrome, que permite a los desarrolladores analizar sus proyectos web en tiempo de
ejecución, complementando las herramientas que el navegador ofrece a los desarrolladores.
Por otra parte, se provee de un mecanismo que, en caso de que haya falsos positivos, las
páginas web continúen funcionando sin problema.
El plugin desarrollado, llamado UFFR Extension , puede ser utilizado desde9
cualquier página web. La misma extiende las funcionalidades de UFFRemover, tales como
instrumentación de archivos JavaScript, detección de funciones sin uso y optimización de
los mismos, pero que tiene como enfoque la optimización de archivos utilizados por
aplicaciones web. La detección de funciones utilizadas, y por ende también las no utilizadas,
se hace de manera automática por la herramienta a medida que la aplicación web sea
utilizada. Esto permite realizar un análisis y captura de datos en tiempo real, que luego son
utilizados en la etapa de optimización de los archivos JavaScript.
Además, como solución al problema de falsos positivos se construyó, dentro de
UFFR Extension, un mecanismo de soft-delete de las funciones detectadas como no
utilizadas durante el proceso de optimización. Es decir, si bien se eliminaránesas funciones
de los archivos, se proveerá una manera de poder cargar y ejecutar su contenido en tiempo
real, en caso de que se requiera.
La extensión provee una interfaz amigable al usuario y de fácil interacción, la cual
permite la previsualización de todos los archivos cargados por el navegador para una fácil
selección de los mismos. También dispone de una UI para ejecutar las funcionalidades
ofrecidas por UFF Remover sin necesidad de utilizar líneas de comando. La extensión
permite:
● Descargar los archivos JavaScript originales que componen una página web
● Seleccionar los archivos JavaScript que el usuario quiera optimizar
● Instrumentar estos archivos mediante un botón, los cuales se descargan en formato
.zip y servirán para reemplazarlos en el servidor web o mediante el uso de una
herramienta externa
● Capturar internamente en un archivo log el uso de las distintas funciones JavaScript
que forman parte de los archivos a optimizar. La extension también permite
descargar este log en formato .txt
● Descargar los archivos optimizados mediante un botón. Este proceso analiza el
archivo log generado, y en base a eso elimina las funciones que no fueron utilizadas
9 https://github.com/leocanalejo/UFFRExtension
10
● Además, implementa un mecanismo de soft delete, para solventar problemas de
errores en caso de falsos positivos (eliminar una función que si era utilizada)
Luego de utilizar esta herramienta durante las pruebas, se obtuvieron archivos
JavaScript más pequeños en tamaño que los originales. En promedio se logró un factor de
compresión de aproximadamente un 80%, que corresponde a un ahorro de tamaño de 2030
KB. Esto favorece el hecho de que se disminuyan los tiempos de descarga y la cantidad de
datos utilizados para aplicaciones distribuidas a través de internet [7], además de disminuir
la memoria requerida, el consumo de energía y el uso de datos móviles en dispositivos
portátiles [3]. Teniendo en cuenta los demás tipos de archivos que componen una página
web, se obtiene en promedio un factor de compresión total del 32.27%.
Figura 1.1: Diagrama de actividades utilizando la extensión de Chrome UFFR Extension
11
En la Figura 1.1 se muestra el diagrama de actividades de la utilización de UFFR
Extension durante el proceso de optimización. La primera actividad es detectar (1) los
archivos utilizados por la página web y seleccionar los que se quieren optimizar. Luego se
realiza la instrumentación (2) de los archivos, es decir, se les agrega una pequeña porción
de código al comienzo de cada una de las funciones, que enviará un mensaje a UFFR
Extension con el fin de saber que esa función ha sido utilizada por la aplicación web. A
continuación se reemplazan (3) los archivos originales por los archivos instrumentados en
el servidor web. Luego, se hace uso de la página web, con el fin de que cuando las
funciones sean ejecutadas se detecten como utilizadas (4) y se guarde ese registro en un
log interno. En el siguiente paso, se optimizan (5) los archivos, eliminando las funciones
que fueron detectadas como no utilizadas durante el paso anterior, tomando como
referencia el log creado. Finalmente, se reemplazan (6) los archivos JavaScript por los
optimizados, con el fin de volver a hacer un renderizado de la página web para realizar
mediciones y detectar posibles errores y falsos positivos.
1.3 Organización del trabajo final
A continuación se describe un breve resumen de los capítulos que componen el
presente trabajo final.
El capítulo 2 describe conceptos teóricos del funcionamiento de una página Web,
comentando algunos aspectos importantes de las optimizaciones en código JS,
mencionando técnicas y herramientas de optimización, haciendo énfasis en las que poseen
optimizaciones dinámicas.
En el capítulo 3 se muestran y describen los principales trabajos de optimización de
código JavaScript, presentando sus aspectos principales, realizando una tabla comparativa
y describiendo las conclusiones a las que se ha llegado y resaltando sus limitaciones.
El capítulo 4 presenta los aspectos correspondientes al diseño e implementación de
la herramienta UFFR Extension, que expande la optimización y resuelve el problema de
falsos positivos.
Luego, el capítulo 5 describe una serie de experimentos realizados con diversas
páginas web para explorar los resultados del enfoque, describe aspectos importantes del
funcionamiento de la herramienta y su desempeño aplicada a casos reales.
Finalmente, en el capítulo 6 se exponen las conclusiones del trabajo final, discute las
limitaciones del enfoque, y también menciona algunas ideas de trabajo a futuro.
12
Capítulo 2: Desarrollo de
aplicaciones JavaScript
El objetivo de este capítulo es introducir conceptos relacionados al desarrollo de
proyectos con el lenguaje JavaScript y sus optimizaciones.
En la sección 2.1 se da una introducción general al lenguaje JavaScript, con una
principal atención en la organización y distribución del código fuente mediante archivos,
mientras que en la sección 2.2 se explican algunos mecanismos y herramientas actuales
para poder optimizar la ejecución de las aplicaciones JavaScript.
2.1 JavaScript como lenguaje de programación
JavaScript es un lenguaje de programación nacido en el año de 1995 dentro del
proyecto para el navegador Netscape Navigator con el objetivo de hacer la web más
dinámica e interactiva. Tal y como fue concebido inicialmente, los programas hechos en
JavaScript son ejecutados en el navegador web, por lo tanto en la computadora del usuario,
y no en el servidor donde se encuentra hospedado el sitio.
JavaScript es el lenguaje de programación encargado de dotar de mayor
interactividad y dinamismo a las páginas web. Cuando JavaScript se ejecuta en el
navegador, no necesita de un compilador, dado que es un lenguaje de programación
interpretado. El navegador lee directamente el código, sin necesidad de terceros, por lo que
resulta sumamente práctico para crear distintos efectos dinámicos. Por tanto, se le reconoce
como uno de los tres lenguajes nativos de la web junto a HTML (contenido y su estructura) y
a CSS (diseño del contenido y su estructura).
Este lenguaje se define como orientado a objetos, basado en prototipos, imperativo,
débilmente tipado y dinámico. Se utiliza principalmente en su forma del lado del cliente
(client-side), implementado como parte de un navegador web permitiendo mejoras en la
interfaz de usuario y páginas web dinámicas, aunque existe una forma de JavaScript del
lado del servidor (Server-side JavaScript o SSJS). Un ejemplo de SSJS es Node.js , que es10
un entorno de ejecución para JavaScript construido con el motor de JavaScript V8 de
Chrome que lo interpreta y lo ejecuta. También es significativo su uso en aplicaciones
externas a la web, por ejemplo en documentos PDF y aplicaciones de escritorio
(mayoritariamente widgets).
En el año 1997 fue adoptado como un estándar ECMA con el nombre de11
ECMAScript, que es una especificación estandarizada de lenguajes de programación. La
misma fue creada para estandarizar JavaScript fomentando múltiples implementaciones
independientes. JavaScript es la implementación más conocida de ECMAScript. Es
comúnmente usado para la programación del código que se ejecutará del lado del cliente.
11 https://www.ecma-international.org
10 https://nodejs.org/es/
13
En aplicaciones JavaScript [1] las funciones son la unidad modular fundamental.
Generalmente están contenidos en archivos de extensión .js. Una función encierra un
conjunto de sentencias y puede invocarse desde otras funciones. Al igual que con otros
lenguajes de programación, las funciones son elementos básicos que permiten reutilizar
código, encapsular comportamiento y realizar composiciones.
Las funciones pueden estar contenidas en módulos. Cómo JavaScript originalmente
no proporcionó mecanismos de modularización, la comunidad de usuarios de JavaScript ha
construido su propio sistema de módulos para ayudar a los desarrolladores a construir
pequeñas unidades independientes de códigoreutilizable a un nivel de abstracción más alto
que las funciones (por ejemplo, usando variables globales, o implementando JavaScript ,12
AMD , UMD , entre otros). En la práctica, la mayoría de los módulos de JavaScript se13 14
implementan en un archivo .js separado y se exportan las funciones que pueden ser
utilizadas por otros módulos, mientras se mantienen las restantes funciones privadas para el
módulo.
Las definiciones de módulos son necesarias para implementar muchos patrones de
diseño y son muy útiles al construir aplicaciones complejas basadas en JavaScript [2].
EcmaScript 6 (ES6 o ES2015) proporciona una definición de módulo integrada (llamada
Harmony) pero todavía no está totalmente soportado por todos los intérpretes JavaScript.
Sin embargo, en este trabajo considera aplicaciones escritas en Harmony ya que pueden
ser “traducidas” (transpiladas ) a EcmaScript 5 (ES5).15
Además, mediante el uso de cualquier definición de módulo (con la excepción de
variables globales), un módulo dado puede especificar dependencias a otros módulos. Este
tipo de dependencia se especifica en el código mediante palabras claves. Por ejemplo en
CommonJS se utiliza la palabra “require”, mientras que en Harmony se utiliza la palabra
“import”.
Las bibliotecas y los frameworks JavaScript se distribuyen comúnmente como un
archivo .js listo para usar. Sin embargo, con la aparición de los sistemas de gestión de
paquetes JavaScript, las bibliotecas también están disponibles como paquetes los cuales
son archivos o directorios que las contienen. Algunos paquetes JavaScript comunes (por
ej.: Chart.js, Moment, Angular, etc.) están disponibles a través de repositorios centrales y
son manejados por gestores de paquetes como NPM y Bower . Son similares a otros16 17
gestores de paquetes como Maven en Java o Rubygems en Ruby.18 19
19 https://rubygems.org/
18 http://maven.org
17 https://bower.io/
16 https://www.npmjs.com/
15 La transpilación es el proceso de transformación y compilación de un lenguaje a otro con un nivel
similar de abstracción
14 https://github.com/umdjs/umd
13 https://github.com/amdjs/amdjs-api/wiki/AMD
12 http://www.commonjs.org
14
Figura 2.1: Cadena de dependencias de Chart.js
Un ejemplo ilustrativo del uso de paquetes se puede ver en la biblioteca Chart.js.
Chart.js es un proyecto para crear gráficos usando HTML5. Como se muestra en la Figura
2.1, la funcionalidad sobre la visualización de fechas y la manipulación del color de Chart.js
se delega a las bibliotecas moment y chartjs-color. Por lo tanto, Chart.js tiene como
dependencias a moment y chartjs-color. Del mismo modo, chartjs-color delega funcionalidad
a otras bibliotecas. Como resultado, varias cadenas de dependencias podrían surgir entre
las bibliotecas.
2.1.1 Sistemas de módulos
Hoy en día las aplicaciones se desarrollan mayormente en base a los conceptos de
encapsulamiento y reúso del software. Es común que alguno de los requerimientos de un
sistema que se está desarrollando, se pueda implementar reutilizando una solución ya
existente. En el instante en que se introduce un módulo ya existente dentro de un nuevo
proyecto, se crea una dependencia entre este proyecto y el módulo utilizado. Dado que
estas piezas necesitan trabajar en conjunto, es importante que no existan conflictos entre
ellas. Entonces, si no se realiza ningún tipo de encapsulamiento, es cuestión de tiempo para
que los módulos entren en conflicto. La encapsulación es esencial para prevenir conflictos y
facilitar el desarrollo [8].
Tradicionalmente en el desarrollo JavaScript del lado del cliente, el chequeo de
dependencias ha sido delegado al desarrollador para facilitar la implementación del
lenguaje. Es decir, siempre ha sido tarea del desarrollador asegurar que las dependencias
se satisfagan al momento de ejecutar cada bloque de código. Así mismo, asegurar que
estas dependencias se carguen en el orden correcto. A medida que las aplicaciones
JavaScript crecen en el tiempo, la gestión de dependencias resulta más difícil y compleja.
15
Los sistemas de módulos (module systems) resuelven este problema y otros más. Ellos
nacen de la necesidad de "acomodar" el creciente ecosistema de JavaScript y facilitar así la
tarea de los desarrolladores.
El problema con los módulos en JavaScript no es el crearlos si no el de cargarlos, ya
que cargar un módulo implica que antes deben estar cargadas sus dependencias y por lo
tanto debemos tener un mecanismo para definir esas dependencias y otro mecanismo para
cargarlas al tiempo que cargamos el módulo.
JavaScript no tiene soporte incorporado para los módulos, pero la comunidad ha
creado muy buenas soluciones alternativas. Las dos normas más importantes (pero
incompatibles) son: CommonJS y AMD, pero también están ES Harmony y UMD. Estos
permiten crear módulos JavaScript, declarar las dependencias (es decir indicar de qué
módulos depende nuestro módulo e incorporar la funcionalidad del módulo del cual
dependemos) y cargar determinados módulos.
El principal atractivo de un módulo es que resulta extremadamente útil para
conseguir código reusable y, sobre todo, modular. Su estructura básica es sencilla: se trata
de una función que actúa como contenedor para un contexto de ejecución. Esto quiere decir
que en su interior, se declaran una serie de variables y funciones que solo son visibles
desde dentro del mismo.
A continuación se describen diferentes estrategias para manejar el concepto de
módulo en JavaScript.
2.1.1.1 Global Variables (variables globales)
Con esta estrategia, primero se revisarán los conceptos básicos del uso de variables
globales aplicadas a la creación de módulos, y luego se cubrirán algunos temas avanzados.
Se mostrará con una visión simple del patrón del módulo.
2.1.1.1.1 Anonymous Closures (cierres anónimos)
Simplemente se crea una función anónima que se ejecutará inmediatamente. Todo
el código que se ejecuta dentro de la función “vive” en un cierre, que proporciona privacidad
y estado durante toda la vida de la aplicación.
(function () {
// code
}());
Figura 2.2: Declaración de una función que se ejecutará inmediatamente
Nótese en la Figura 2.2 los paréntesis alrededor de la función anónima. Esto es
requerido por el lenguaje, ya que las declaraciones que comienzan con el símbolo function
siempre se consideran declaraciones de funciones. Si se incluyen paréntesis, en lugar de
eso, se crea una expresión por la cual se declara una función, que será llamada
inmediatamente. La función crea un nuevo ámbito y crea "privacidad". JavaScript no tiene
16
privacidad, pero la creación de un nuevo ámbito emula esto cuando envuelve toda nuestra
lógica de función dentro de ellos. La idea entonces es devolver sólo las partes que se
necesitan, dejando el otro código fuera del alcance global.
2.1.1.1.2 Global Import (importación global)
JavaScript tiene una característica conocida como implied globals. Cada vez que
se utiliza un nombre, el intérprete se mueve por la cadena de ámbitos hacia arriba en busca
de una declaración de variable para ese nombre. Si no se encuentra ninguno, esa variable
se supone que es global. Si se usa en una asignación, la variable global se crea si ya no
existe. Esto significa que el uso o la creación de variables globales en un anonymous
closure es fácil. Desafortunadamente, esto conduce a administrar el código de manera
bastante difícil, ya que no es evidente (para los programadores) qué variables son globales
o no en un archivo determinado.
Por suerte, la función anónima ofrece una alternativa fácil. Por medio del pasaje de
variables globales como parámetros a la función anónima (Figura 2.3), se importan al
código, que además es a la vez más claro y más rápido que usando implied globals.
(function ($, Math) {
// code
}(jQuery, Math));
Figura 2.3: Funciona anónima con variables globales como parámetros
2.1.1.1.3 Module Export (módulo de exportación)
A veces no sólo se quieren usar variables globales, sino que se quieren declararlas.
Es posible hacer esto fácilmente mediantesu exportación, utilizando el valor de retorno de
la función anónima.
var module = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// code
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// code
};
return my;
}());
Figura 2.4: Declaración del módulo module
17
En la Figura 2.4, se puede apreciar que se ha declarado un módulo global llamado
module, con dos propiedades comunes, un método llamado module.moduleMethod y una
variable llamada module.moduleProperty. Además, se mantiene el estado interno privado
utilizando el cierre de la función anónima. También, es posible importar fácilmente globales
necesarios, utilizando el patrón que se ha mostrado anteriormente.
2.1.1.2 Advanced Patterns (patrones avanzados)
A partir de lo que hemos visto, es posible utilizar este patrón y hacerlo llegar un poco
más lejos, creando algunas construcciones muy potentes y extensibles. A continuación se
utilizará, a modo de ejemplo, un módulo llamado module.
2.1.1.2.1 Augmentation
Una limitación del patrón de módulo hasta ahora es que todo el módulo debe estar
en un archivo. Cualquier persona que ha trabajado en una gran base de código entiende el
valor de la división entre varios archivos. Existe una buena solución para aumentar los
módulos. Primero, se importa el módulo, luego se añaden propiedades, luego se exporta.
En la Figura 2.5, se aprecia cómo se aumenta el módulo module desde “arriba”.
var module = (function (my) {
my.anotherMethod = function () {
// code
};
return my;
}(module));
Figura 2.5: Augmentation de un module desde arriba
Se utiliza nuevamente la palabra clave ‘var’ para mantener la coherencia, aunque no
es necesario. Después de ejecutar este código, el módulo habrá obtenido un nuevo método
público denominado module.anotherMethod. Este archivo de aumento también mantendrá
su propio estado interno privado y las importaciones.
2.1.1.2.2 Loose Augmentation
Mientras que el ejemplo anterior requiere que la creación del módulo inicial sea la
primera, y que el incremento suceda en segundo lugar, esto no siempre es necesario. Una
de las mejores cosas que una aplicación JavaScript puede hacer por el rendimiento es
cargar scripts asincrónicamente. Se pueden crear módulos flexibles de varias partes que se
pueden cargar en cualquier orden con un “loose augmentation”. Cada archivo debe tener la
estructura de la Figura 2.6.
18
var module = (function (my) {
// code
return my;
}(module || {}));
Figura 2.6: Estructura de un módulo para aceptar augmentation asíncrono
En este modelo, la palabra ‘var’ es siempre necesaria. Hay que tener en cuenta que
la importación creará el módulo si aún no existe.
2.1.1.2.3 Tight Augmentation
Mientras que el patrón loose augmentation es excelente, pone algunas limitaciones
en su módulo. Lo más importante, no puede anular las propiedades del módulo de forma
segura. También no puede utilizar las propiedades de módulo de otros archivos durante la
inicialización (pero si se puede en tiempo de ejecución). Tight augmentation implica un
orden de carga establecida, pero permite anulaciones. En la Figura 2.7 se presenta un
ejemplo simple aumentando el módulo original que se viene utilizando.
var module = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// sobreescritura del método, tiene acceso al anterior a través
// old_moduleMethod
};
return my;
}(module));
Figura 2.7: Ejemplo de anulación de un método utilizando tight augmentation
Aquí se ha anulado el método module.moduleMethod, pero se mantiene una
referencia al método original, si es necesario.
2.1.1.2.4 Cloning and Inheritance (clonación y herencia)
Este patrón es quizás la opción menos flexible. Las propiedades que son objetos o
funciones no se duplicarán, existirán como un objeto con dos referencias. Cambiar uno
cambiará el otro. Esto podría ser fijo para objetos con un proceso de clonación recursiva,
pero probablemente no se puede usar para funciones, excepto quizás con eval. Se puede
encontrar un ejemplo en la Figura 2.8.
19
var module_two = (function (old) {
var my = {}, key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var super_moduleMethod = old.moduleMethod;
my.moduleMethod = function () {
// sobreescribe método en el clone, accede al super a través de
// super_moduleMethod
};
return my;
}(module));
Figura 2.8: Ejemplo de clonación y herencia
2.1.1.2.5 Sub-modules
El patrón final es realmente el más simple, es como crear módulos regulares, (Figura
2.9). Hay muchas situaciones en las que es útil para crear submódulos. Los submódulos
tienen todas las capacidades avanzadas de los módulos normales, incluyendo el incremento
y el estado privado.
module.sub = (function () {
var my = {};
// ...
return my;
}());
Figura 2.9: Ejemplo de definición de submódulo
2.1.1.3 Revealing Module
A medida que las aplicaciones JavaScript fueron haciéndose más complejas, el
patrón de programación Revealing Module o "Módulo Revelador" comenzó a usarse cada
vez con mayor frecuencia en JavaScript. La estructura general es idéntica a la del módulo
tradicional con la excepción de que todos los métodos quedan declarados en el ámbito
cerrado del objeto, sólo aquellos que se necesita que sean de acceso externo, son
referenciados en el interior del bloque “return”.
20
var module = (function () {
var nombre = "Leonardo",
saludo = "Hola!";
// función privada
function imprimirNombre() {
console.log("Nombre: " + nombre);
}
// función pública
function asignarNombre(nuevoNombre) {
nombre = nuevoNombre;
}
// Revelar accesos públicos (opcionalmente con otros nombres)
return {
setName: asignarNombre;
greeting: saludo
};
})();
module.setName("Micaela");
Figura 2.10: Ejemplo del patrón Revealing Module
Los ámbitos en JavaScript siempre fueron a nivel de función (hasta antes de la
aparición de let en ES2015). Esto significa que todo lo que se declara dentro de una función
no puede escapar de su ámbito. Es por esta razón que, el patrón Revealing Module se basa
en funciones para encapsular el contenido privado (como muchos otros patrones de
JavaScript).
La Figura 2.10 muestra un ejemplo del patrón Revealing Module. Las funciones y
variables públicas son expuestas en el objeto devuelto (al final con un return). Todas las
otras declaraciones están protegidas por el ámbito de la función que las contiene. Se debe
tener en cuenta que la variable no está recibiendo la función directamente, sino más bien el
resultado de ejecutar la función, es decir, el objeto que se devuelve a través del return de la
función anónima. Esto se conoce como "Immediately-invoked function expression".
2.1.1.4 CommonJS
CommonJS es un proyecto que define una serie de especificaciones para el
ecosistema de JavaScript, fuera del navegador (por ejemplo, en el lado del servidor o para
aplicaciones de escritorio). Una de las áreas que el equipo de CommonJS intenta abordar
son los módulos en JavaScript. Los desarrolladores de Node.js originalmente intentaron
21
seguir la especificación de CommonJS, pero luego cambiaron de decisión. En lo que se
refiere a módulos, la implementación en Node.js se vio influenciada.
Existen abstracciones sobre el sistema de módulos de Node.js, en forma de
bibliotecas, que actúan como un puente entre los módulos de Node.js y CommonJS. Tanto
en Node como en CommonJS, existen 2 palabras esenciales para interactuar con los
módulos: require y exports. La función require se puede usar para importar símbolos
desde otro módulo al ámbito actual. El parámetro pasado a require es el “id” del módulo. En
la implementación de Node, es el nombre del módulo dentro de la carpeta node_modules
(o, en todo caso, la ruta hacia su ubicación). El objeto exports es especial: todo lo que es
puesto en él se puede exportar como un elemento público (conservando el nombre de los
elementos).
Los módulos en CommonJS fueron diseñados teniendo en mente el desarrollo del
lado del servidor. CommonJSes un sistema de módulos síncrono: es decir, la carga de
módulos es un proceso síncrono que empieza por un módulo inicial. Al cargarse este
módulo se cargarán todas sus dependencias (y las dependencias de las dependencias, y
así hasta cualquier nivel de profundidad). Una vez finalicen todas esas cargas, el módulo
inicial está cargado y empieza a ejecutarse.
// Complex.js
var Complex = function (r, i) {
this.r = r instanceof Complex ? r.r : r;
this.i = r instanceof Complex ? r.i : (i || 0);
}
module.exports = Complex;
Figura 2.11: Ejemplo de la sintaxis de CommonJS
Definir un módulo en formato CommonJS se realiza de la forma mostrada en la
Figura 2.11. Este código define un módulo que exporta una función (constructora) llamada
Complex. Se utiliza module.exports para indicar qué es lo que exporta el módulo. Todo lo
que no pertenezca al exports son variables (y funciones) privadas del módulo. Ahora se
podría declarar otro módulo que dependiera de este módulo, como se muestra en la Figura
2.12.
22
// math.js
var Complex = require('./complex');
addComplex = function (ca, cb) {
return new Complex(ca.r + cb.r, ca.i + cb.i);
}
var math = {
add: function (a, b) {
if (a instanceof Complex || b instanceof Complex) {
return addComplex(new Complex(a), new Complex(b));
}
return a + b;
}
}
module.exports = math;
Figura 2.12: Ejemplo de dependencia entre módulos con CommonJS
Este módulo math.js requiere el módulo complex.js (de ahí el uso de require),
define un objeto math con un método y exporta dicho objeto. La función addComplex es
privada al módulo.
Finalmente, se puede crear un tercer módulo (main.js) que use esos módulos para
sumar tanto números reales como complejos, como puede apreciarse en la Figura 2.13.
// main.js
var Complex = require('./complex');
var math = require('./math');
console.log(math.add(40, 2));
var c1 = new Complex(40, 3);
console.log(math.add(c1, 2))
Figura 2.13: Ejemplo de múltiple dependencia entre módulos con CommonJS
2.1.1.5 Asynchronous Module Definition (AMD)
AMD es otra especificación de módulos JavaScript, cuya principal diferencia con
CommonJS es que es asíncrona (AMD significa Asynchronous Module Definition). La
implementación más conocida para navegadores de AMD es RequireJS. Al ser asíncrona
permite la carga de módulos bajo demanda (es decir cargar un módulo sólo si se va a usar),
lo que puede ser interesante según en qué aplicaciones.
define(
module_id /*opcional*/,
[dependencies] /*opcional*/,
definition function /*función para instanciar el módulo u objeto*/
);
Figura 2.14: Ejemplo de la sintaxis de AMD
23
La carga asíncrona en JavaScript es posible usando closures: una función es
llamada cuando los módulos requeridos terminan de cargar. La definición e importación de
módulos se lleva a cabo por la misma función: cuando se define un módulo se indican sus
dependencias de forma explícita. De esta forma, un cargador AMD puede tener una imagen
completa del grafo de dependencias para un proyecto determinado en tiempo de ejecución.
Las bibliotecas que no dependen de otras pueden ser cargadas al mismo tiempo.
Esto es muy importante para los navegadores, donde el tiempo de carga inicial es un punto
esencial para brindar una buena experiencia de usuario.
Como se muestra en la Figura 2.14, los módulos AMD empiezan con una llamada a
define, que acepta básicamente tres parámetros: un “identificador del módulo”, un array con
las dependencias del módulo (el equivalente al require de CommonJS) y luego una función
con el código del módulo. Esta función devuelve lo que el módulo exporta (es decir, el
return de la función equivale al module.exports de CommonJS).
Para conocer en detalle cómo es la sintaxis para definir un módulo AMD se puede
seguir el ejemplo de la Figura 2.15.
// Complex.js
define([], function () {
console.log('complex loaded...');
var Complex = function (r, i) {
this.r = r instanceof Complex ? r.r : r;
this.i = r instanceof Complex ? r.i : (i || 0);
}
return Complex;
});
// math.js
define(['complex'], function (Complex) {
addComplex = function (ca, cb) {
return new Complex(ca.r + cb.r, ca.i + cb.i);
}
var math = {
add: function (a, b) {
if (a instanceof Complex || b instanceof Complex) {
return addComplex(new Complex(a), new Complex(b));
}
return a + b;
}
}
return math;
});
// main.js
define(['complex', 'math'], function (Complex, math) {
console.log(math.add(40, 2));
var c1 = new Complex(40, 3);
console.log(math.add(c1, 2));
});
Figura 2.15: Ejemplo guia del uso de AMD
24
El módulo que define Complex no depende de nadie, así que el array está vacío.
Siguiendo con el ejemplo, el módulo math sí contendrá dependencias y por lo tanto serán
definidas en el array. Ahora este módulo depende del módulo Complex. Eso significa que al
cargarse este módulo, el módulo Complex (archivo Complex.js) debe estar cargado. Si no
está cargado, RequireJS lo cargará asíncronamente, y cuando esta carga haya finalizado
invocará la función que define el módulo. Se observa que ahora la función tiene un
parámetro. Este parámetro se corresponde con lo que exporta (devuelve) el módulo
complex del cual dependemos. Básicamente, por cada elemento (dependencia) del array
tendremos un parámetro en la función. Eso se ve todavía más claro en el módulo main.js
que depende tanto de complex como de math. Se observa que hay dos parámetros en el
array de dependencias y por lo tanto la función del módulo recibe dos parámetros. El array
indica las dependencias y el parámetro de la función permite acceder a ellas.
Finalmente, tan solo resta tener un HTML que cargue primero RequireJS y, una vez
que haya terminado, indicar a RequireJS que cargue el módulo main.js y lo ejecute. Al
cargar este módulo, RequireJS cargará asíncronamente todas las dependencias.
2.1.1.6 Módulos en ES2015 (ECMAScript 6)
El objetivo de los módulos de ES Harmony fue crear un formato para que los
usuarios de CommonJS y de AMD estén satisfechos. Al igual que CommonJS, tienen una
sintaxis compacta, una preferencia por las exportaciones individuales y el apoyo a las
dependencias cíclicas. Y al igual que AMD, tiene soporte directo para la carga asíncrona y
la carga configurable de módulos.
Los módulos ES Harmony se almacenan en archivos. Hay exactamente un módulo
por archivo y un archivo por módulo. Existen dos tipos de exportaciones: las exportaciones
con nombre (se soporta varias por módulo) y las exportaciones por defecto (una por
módulo). Estas dos formas se pueden mezclar, pero normalmente es mejor usarlas por
separado.
El equipo de ECMA (encargado de la estandarización de JavaScript) decidió abordar
el tema de los módulos. Esto se puede ver en la última versión del estándar JavaScript:
ECMAScript 2015 (anteriormente conocido como ECMAScript 6). El resultado es
sintácticamente agradable, y compatible con ambos modos de operación (de forma síncrona
y asíncrona).
Como se ve en la Figura 2.16, la directiva import permite traer módulos al ámbito
actual. Esta directiva, en contraste con require y define, no es dinámica (es decir, no se
puede llamar en cualquier lugar). La directiva export, por otro lado, puede usarse para
explícitamente hacer públicos los elementos. La naturaleza estática de import y export
permite a los analizadores estáticos construir un árbol completo de las dependencias sin
ejecutar código.
Un módulo puede exportar varias cosas prefijando sus declaraciones con la palabra
clave export. Estas exportaciones e importaciones se distinguen por sus nombres, aunque
25
también se puede importar todo el módulo completo utilizando asterisco (‘*’). Ambos
ejemplos se encuentran en la Figura 2.16.
// lib.js
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
// main.js
import { square, diag } from 'lib'; // importac
import * as lib from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
Figura 2.16: Ejemplo de sintaxis de ES2015(también conocido como ES6)
Los módulos que sólo exportan un solo valor son muy comunes en Node.js, pero
también son comunes en el desarrollo de frontend, donde a menudo existen
constructores/clases para modelos, con un modelo por módulo. Las exportaciones por
defecto son especialmente fáciles de importar (Figura 2.17).
// myFunc.js
export default function () { ... };
// main.js
import myFunc from 'myFunc';
myFunc();
Figura 2.17: Ejemplo de exportación/importación por defecto en ES2015
Existen diferentes formas de importación provistas por ES Harmony, todas ellas se
expresan en la Figura 2.18.
26
// Importaciones por defecto e importaciones con nombre
import theDefault, { named1, named2 } from 'src/mylib';
import theDefault from 'src/mylib';
import { named1, named2 } from 'src/mylib';
// Renombre: importar named1 como myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib';
// Importando el módulo como un objeto
// (Con una propiedad por exportación con nombre)
import * as mylib from 'src/mylib';
// Sólo se carga el módulo, no se importa nada
import 'src/mylib';
Figura 2.18: Ejemplos de importaciones en ES2015
2.1.1.7 UMD (Universal Module Definition)
Dado que los estilos CommonJS y AMD han sido igualmente populares y además
parece que aún no se ha decidido por uno o por otro, se ha provocado el surgimiento de un
patrón "universal" que apoye ambos estilos.
El patrón es ciertamente difícil en su escritura de módulos, como puede apreciarse
en la Figura 2.19, pero es compatible tanto con AMD como con CommonJS, así como
también con la definición de variables globales tradicionales.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// methods
function myFunc(){};
// exposed public method
return myFunc;
}));
Figura 2.19: Ejemplo de sintaxis de UMD
27
2.1.2 Distribución de aplicaciones Javascript
A pesar de las ventajas de usar bibliotecas externas, el uso de las funciones de una
biblioteca depende de que las bibliotecas que están contenidas en esta se incluyan en el
entorno de ejecución. En este esquema, existen principalmente dos estrategias de
implementación. La primera estrategia es permitir que el usuario final descargue e instale
las bibliotecas necesarias. Esta estrategia es poco deseable porque hace que el proceso de
instalación sea más lento y propenso a errores. Por ejemplo, en la Figura 2.1, el usuario que
necesita Chart.js debe descargar e instalar “moment” y “chartjs-color”. Por otra parte, los
usuarios pueden instalar inadvertidamente una versión incompatible de “moment” o
“chartjs-color”, que puede causar fallas inesperadas. También, en las aplicaciones que se
ejecutan en un navegador web, la descarga de las bibliotecas por separado aumenta el
número de solicitudes HTTP, lo que afecta el tiempo de carga de la página web [9].
La segunda estrategia es agrupar las bibliotecas necesarias junto con la aplicación
que las usa. En el ejemplo de la Figura 2.1, significaría empaquetar las bibliotecas moment
y chartjscolor con el código de Chart.js. Si bien este segundo enfoque tiene menos
probabilidades de incurrir en errores de versión o disminuir las solicitudes HTTP, el tamaño
del paquete puede incrementar significativamente. En el ejemplo, esto significa que el
tamaño de Chart.js con el código fuente de todas sus dependencias es significativamente
más grande que solo el código fuente de Chart.js (sin bibliotecas). Por lo tanto, se hace una
sola descarga de un archivo grande en lugar de muchas descargas pequeñas.
2.1.3 Minificación y Bundling
A lo largo de los años, los desarrolladores JavaScript de front-end han elegido la
segunda estrategia [4]. Una razón para esto es que una sola solicitud HTTP de un archivo
grande es probable que sea menos costosa que muchas solicitudes HTTP de archivos
pequeños. En este contexto, en un proceso de desarrollo JavaScript típico, las bibliotecas
de terceros se empaquetan con el código fuente que se desarrolla antes de un lanzamiento.
Esta fase de empaquetado se compone principalmente de procesos de Minificación y
Bundling (Figura 2.20). Estos procesos son importantes para mejorar la implementación y20
reducir el tamaño final de la aplicación. Reducir el tamaño de las aplicaciones JavaScript es
importante porque disminuye tiempos de descarga y la cantidad de datos para aplicaciones
distribuidas a través de Internet [7]. Además, disminuye la memoria requerida y el consumo
de energía en dispositivos móviles [3].
20 Consiste en combinar los archivos y módulos de la aplicación
28
Figura 2.20: Descripción del proceso de desarrollo de un paquete JavaScript
El proceso de Bundling es la concatenación de todos los archivos JavaScript en un
solo archivo llamado "bundle". El bundle contiene todo el código de la aplicación y las
bibliotecas requeridas en esta. Algunas herramientas de Bundling (como Browserify y
Webpack) aprovechan este proceso para reducir el código. Por ejemplo, estas herramientas
pueden descartar módulos que no son requeridos por cualquier otro módulo (al observar las
relaciones de los módulos requeridos) o evitar incluir en el bundle un paquete que no es
necesario y se incluyó erróneamente como una dependencia del proyecto.
El proceso de Bundling es seguido por el proceso de Minificación. La Minificación se
basa principalmente en técnicas de texto (por ejemplo, cambiar nombres, eliminar espacios,
saltos de línea y comentarios) para reducir el tamaño final del bundle.
A pesar de las reducciones que se pueden hacer durante el proceso de Bundling,
una aplicación con código no utilizado puede ser problemático si un paquete es grande y
contiene una gran cantidad de código que no es necesario en el contexto de la aplicación en
construcción. Eliminar código no utilizado es un desafío, parcialmente debido a la naturaleza
del tipado dinámico de JavaScript. Determinar si una función es llamada o no suele
depender de la ejecución de la aplicación y en el contexto en el que esta se ejecuta [2, 4].
En este contexto, monitorear las ejecuciones de JavaScript para identificar el código no
utilizado puede en gran medida complementar las herramientas de agrupamiento actuales
para reducir el tamaño de los paquetes.
2.2 Resumen
Con el paso de los usuarios de dispositivos de escritorio a dispositivos móviles, la
optimización de las páginas web en el cliente cobra mayor interés que en el pasado,
equiparando en importancia a las optimizaciones en el servidor. Lograr que una aplicación
web funcione rápido en unos dispositivos que son más lentos y con conexiones que,
dependiendo de la cobertura, pueden ser malas o incluso sufrir cortes, no es una tarea fácil.
Pero es necesario, puesto que disponer de mayor velocidad implica usuarios más
satisfechos que visitarán más a menudo.
Los sistemas de módulos para Javascript surgen como una necesidad de los
mismos programadores, de encapsular distintas funcionalidades en "bloques de código"
29
reutilizables. Estos bloques son llamados módulos y es importante contar con un
mecanismo para gestionar las dependencias entre estos módulos. Si bien esta gestión de
dependencias hará que solo se utilicen los que son requeridos, es muy probable que no
todas las funciones contenidas en ellos sean realmente utilizadas por las páginas web. Se
necesita de un mecanismo pueda detectar y eliminar estas funciones.
Existen puntos en los que hay que prestar mayor atención a la hora de la
optimización del código JavaScript que se ejecuta en el cliente. Uno de los más importantes
es la optimización en la fase de compilación para acelerar su descarga. En esta etapa se
aplican las técnicas mencionadas de Bundling y Minificación, para lograr una reducción del
tamaño del bundle. Esto se puede complementareliminando el código que nunca es
ejecutado. Sin embargo, esto es complejo debido al dinamismo del lenguaje y depende del
contexto en el que se utilice la aplicación.
30
Capítulo 3: Trabajos relacionados
En este capítulo se describen los trabajos relacionados más relevantes de
optimización de código JavaScript. Con este objetivo, en la Sección 3.1 se presentan los
herramientas más significativas en el área, en la Sección 3.2 se realiza un análisis
comparativo de estas aplicaciones y, por último, en la Sección 3.3 se presentan las
conclusiones a las que se ha arribado, resaltando las falencias de los enfoques descritos en
el dominio de aplicaciones de optimización de código Javascript.
3.1 Herramientas relevantes
Desde que se comenzaron a desarrollar aplicaciones más complejas utilizando
JavaScript, surgió la necesidad de contar con más herramientas que den mejor soporte a
los desarrolladores [6]. Existen muy pocos estudios empíricos sobre el análisis de funciones
de biblioteca no utilizadas en desarrollos JavaScript. En este contexto, los principales
esfuerzos se han centrado en tener una infraestructura de análisis estático para el lenguaje
completo como se define en el estándar ECMAScript.
3.1.1 Lazy propagation
Cuando se pretende analizar el flujo de una aplicación escrita en JavaScript, surgen
problemas al analizar los tipos de las funciones debido al dinamismo del lenguaje. Lazy
Propagation [10, 11] es una técnica para mejorar el rendimiento del análisis interprocedural
en situaciones donde los métodos existentes, como IFDS [18], framework monótono [19] o
el enfoque funcional [17], no se aplican. Esta técnica busca evitar gran parte de la
redundancia que se genera cuando funciones simples se analizan una gran cantidad de
veces mientras se preserva, o incluso mejora, la precisión del análisis.
Las contribuciones de esta técnica se pueden resumir de la siguiente manera.
Primero propone una adaptación basada en ADT (tipo de dato abstracto) del Framework
monótono a lenguajes de programación con estructuras de heap mutables y first class
functions . Segundo, propaga información del flujo de datos "por necesidad" en un21
algoritmo iterativo de punto fijo.
La idea principal es introducir una noción de valores "desconocidos" para los
campos de objeto a los que no se accede dentro de la función actual. Esto evita que mucha
información irrelevante se propague durante el cálculo del valor de las funciones. El análisis
inicialmente supone que no se accede a los campos cuando el flujo ingresa a una función.
Cuando se lee un valor desconocido, se invoca una operación de recuperación para volver
a través del grafo de llamadas y propagar el valor correcto. Al evitar recuperar los mismos
21 First Class Function significa que podemos tratar a una función en Javascript como cualquier otro
tipo de dato: asignarlas a variables, pasarlas como argumentos a otras funciones, y crearlas al vuelo.
31
valores repetidamente, el costo total amortizado de recuperación nunca es mayor que el del
análisis original. Con grandes estados abstractos, el mecanismo hace una diferencia
notable en el rendimiento del análisis.
function Person(n) {
this.setName(n);
}
Person.prototype.setName = function(n) {
this.name = n;
}
function Student(n, s) {
Person.call(this, n);
this.studentId = s.toString();
}
Student.prototype = new Person;
var x = new Student("James P. Sullivan", 1234);
x.setName("Mike Wazowski");
Figura 3.1: Código ejemplo para Lazy propagation
El código de la Figura 3.1 define dos "clases" Person y Student con sus propios
constructores. Person tiene un método setName a través de su objeto prototipo, y Student
hereda setName y define un campo adicional studentId. La declaración de llamada en
Student invoca al constructor de superclase Person. Si, por ejemplo, se llama al método
setName desde otras ubicaciones en el programa, entonces el más mínimo cambio de
cualquier estado abstracto que aparezca en cualquier sitio de llamada de setName durante
el análisis provocaría que el método se volviera a analizar, incluso aunque los cambios
pueden ser completamente irrelevantes para ese método.
Lazy Propagation no debe confundirse con el análisis basado en la demanda [11]. El
objetivo de este último es calcular los resultados de un análisis solo en puntos específicos
del programa, evitando así el esfuerzo de calcular un resultado global. Lazy propagation
calcula un modelo del estado para cada punto del programa. Sin embargo, hacer un análisis
estático del flujo no es suficiente, debido a que el flujo de una aplicación JavaScript puede ir
cambiando dinámicamente durante la ejecución. Además, con la aplicación de esta técnica
no se mejora la performance de la aplicación ni se elimina el código sin uso de la misma.
3.1.2 Modelado del HTML DOM y de la API del navegador
Este trabajo [12, 13, 14] presenta un análisis estático que intenta determinar de
forma precisa el flujo de control y el flujo de datos en aplicaciones JavaScript que se
ejecutan en un navegador. Su implementación fue realizada como una extensión de la
Herramienta TAJS [15] y modela tanto la estructura DOM del HTML y API del navegador.
Esto incluye la jerarquía de objetos de HTML y el modelo de ejecución dirigido por eventos.
32
Puntualmente se logran mejoras en los siguientes aspectos: (1) el análisis puede
mostrar la ausencia de errores de programación comunes en los benchmark (programas de
referencia), (2) el análisis puede ayudar a detectar posibles errores, como nombres de
propiedad mal escritos, (3) los grafos de llamadas obtenidos son precisos ya que la mayoría
de llamadas son monomórficos, (4) los tipos obtenidos son precisos porque en muchas
expresiones se determina que tienen tipos únicos y (5) el análisis puede identificar código
muerto y las funciones inalcanzables. Dicha información puede proporcionar una base para
lograr una mejor herramienta de soporte para desarrolladores de aplicaciones web
JavaScript.
El problema que presenta el análisis estático es que a veces, determinar un tipo
suele no ser tan fácil debido a que puede cambiar el tipo de una variable según como se
invoque una función. Lo mismo ocurre tratando de identificar el código muerto. La detección
de errores de sintaxis ayuda al momento de escribir el código, pero no tienen impacto en la
performance de la aplicación.
3.1.3 JSNOSE
Fard y Mesbah [16] presentan una técnica de detección code smells JavaScript22
llamada JSNOSE. Esta técnica basada en métricas, combina análisis estático y dinámico
para detectar code smells del lado del cliente. Proponen un conjunto de 13 code smells
JavaScript, recopilados de varios recursos de desarrollador.
La Figura 3.2 muestra una visión general del enfoque. (1) La configuración, que
contiene las métricas definidas y umbrales, alimenta al detector de code smells.
Automáticamente (2) se intercepta el código JavaScript de una web determinada aplicación,
configurando un proxy entre el servidor y el navegador, (3) se extrae el código JavaScript de
todos los archivos .js y HTML archivos, (4) se mapea el código fuente en un árbol de
sintaxis abstracta (AST) y se analiza recorriendo el mismo. Durante el recorrido del AST, el
analizador visita todas las entidades, objetos, propiedades, funciones y bloques de código
del programa, y almacena su estructura y relaciones. Al mismo tiempo, (2) se instrumenta el
código para monitorear la cobertura del árbol, que se utiliza para la detección de código
muerto o no utilizado. A continuación, (7) se navega por la aplicación instrumentada para
producir un seguimiento de ejecución, a través de un rastreador dinámico automatizado, y
(8) recopilar y usar trazas de ejecución para calcular la cobertura del código. Se (5) extraen
patrones del AST como nombres de objetos y funciones, y (6) se infieren objetos JavaScript,
sus tipos y propiedades dinámicamente al consultar el navegador en tiempo de ejecución.
Finalmente, (9) en función de todos los datos estáticos y dinámicos recopilados, se detectanlos code smells (10) usando las métricas
Una de las principales desventajas de esta técnica es que solo analiza el código
escrito, no así bibliotecas, lo que no reduce el tamaño del bundle. Otra desventaja es que
22 Patrones en el código fuente que pueden influir negativamente en la comprensión del programa y
la mantenibilidad del mismo a largo plazo.
33
solo reporta los code smells detectados, sin realizar ningún tipo optimización automática,
por lo tanto, el desarrollador deberá tener intervención manual en la optimización.
Figura 3.2: Vista de procesamiento de JSNOSE, detector de code smells JavaScript
3.1.4 Browserify
Browserify es una herramienta open source que permite crear módulos en el cliente,
utilizando la misma sintaxis que en Node (CommonJS ). Por lo tanto, se puede requerir y23
exportar módulos y manejar sus dependencias como en Node pero en el navegador.
Figura 3.3: Uso de Browserify
23 http://www.commonjs.org/
34
En la Figura 3.3 se muestra un ejemplo del uso de Browserify, en la misma se puede
ver los archivos de Node escritos en ES6 y cómo mediante Browserify se puede unificar los
mismos en un archivo Bundle.js y así solo tener que enlazar un solo script en el HTML.
Babel es el transpilador que se encarga de la traducción del código.24
Se puede usar Browserify para organizar su código y usar bibliotecas de terceros.
Una gran ventaja es que permite usar NPM para instalar y manejar las dependencias de los
módulos. Por lo que es posible “requerir” cualquier módulo que se encuentre publicado en
NPM, o bien, utilizar módulos privados.
El sistema de módulos que usa browserify es el mismo que el de Node, por lo que
los paquetes publicados en NPM que originalmente estaban destinados a usarse en Node
pero no en los navegadores también funcionarán bien en el navegador. Si bien la utilización
de módulos y bibliotecas de terceros facilita mucho la creación de nuevas aplicaciones,
provoca que el bundle resultante de la unificación de los mismos sea innecesariamente
grande. Muchas veces se importa una biblioteca muy grande cuando solo es necesaria muy
poca funcionalidad de la misma.
3.1.5 Webpack
Webpack es un empaquetador de módulos (del inglés, "module bundler") JavaScript
de código abierto. Principalmente para JavaScript, pero puede transformar otros archivos
del front-end como HTML, CSS e imágenes si se incluyen los complementos
correspondientes. Webpack toma módulos con dependencias y genera activos estáticos
que representan esos módulos.
Figura 3.4: Empaquetado con Webpack
Webpack toma las dependencias y genera un grafo que permite a los
desarrolladores web utilizar un enfoque modular para sus propósitos de desarrollo de
aplicaciones web. Se puede usar desde la línea de comandos, aunque lo habitual es utilizar
24 https://babeljs.io/
35
un archivo de código especial llamado webpack.config.js que está en la raíz de tu proyecto
de desarrollo y que define mediante código JavaScript las operaciones que quieres realizar,
como definir reglas, complementos, etc., para un proyecto. Webpack es altamente
extensible a través de reglas que permiten a los desarrolladores escribir tareas
personalizadas que desean realizar al agrupar archivos.
Tiene soporte de serie para minimizar o combinar archivos, generar mapas de
depuración o copiar recursos, e incluso ofrece un servidor web integrado con la capacidad
de recarga automática cuando algo cambia o reemplazo en caliente de módulos. Las pocas
cosas para las que no tiene soporte directo o a través de loaders o plugins se pueden
conseguir con el uso de scripts npm, al fin y al cabo la herramienta está basada en Node.js
y necesita npm para instalarse.
Entre sus ventajas se encuentra la eliminación de recursos no utilizados, que
permite de manera sencilla obtener para producción únicamente aquellos recursos que son
necesarios, dejando de lado los que no se utilizan. Esto incluye, por ejemplo, las reglas CSS
que de verdad se están utilizando, dejando fuera las demás, lo cual es una utilidad muy
potente. Además se puede utilizar sobre cualquier tipo de sistema de modularización para
JavaScript, sea AMD, CommonJS, ES2015 o Angular, sin preocuparte de que el navegador
tenga que soportarlo (Browserify, por ejemplo, solo soporta CommonJS, que es el de
Node.js).
El mayor problema que ha tenido Webpack desde siempre es que la configuración
es un tanto complicada, al menos al principio, lo que hace que el desarrollador tenga que
esperar un poco en tenerlo funcionando para tu proyecto. Además, todo lo se utilice debe
ser modular, no sólo deberá escribir los archivos JavaScript como módulos, sino que las
dependencias de otras bibliotecas de JavaScript de terceros que utilice deben ser
modulares también. En cuanto a tamaño resultante tiene las mismas desventajas que
Browserify, muchas veces los bundles resultantes son muy grandes debido a las bibliotecas
importadas.
3.1.6 Rollup
Rollup es un empaquetador de módulos que compila pequeños fragmentos de
código en algo más grande y complejo, como una biblioteca o aplicación. Utiliza el nuevo
formato estandarizado para los módulos de código incluidos en la revisión ES6 de
JavaScript, en lugar del formato de soluciones anteriores como CommonJS y AMD. Los
módulos ES permiten combinar libre y sin problemas las funciones individuales más útiles
de las bibliotecas importadas.
La revisión ES6 de JavaScript incluye una sintaxis para importar y exportar
funciones y datos para que puedan compartirse entre distintos archivos JavaScript. Rollup
permite escribir su código usando el nuevo sistema de módulos, y luego lo compila
nuevamente a los formatos compatibles existentes, como los módulos CommonJS, los
módulos AMD y los scripts de estilo IIFE. Además de habilitar el uso de módulos ES, Rollup
también analiza estáticamente el código que está importando y excluirá todo lo que no se
36
use realmente. Esto permite construir sobre las herramientas y módulos existentes sin
agregar dependencias adicionales o aumentar el tamaño de su proyecto. Debido a que
Rollup incluye lo mínimo, da como resultado bibliotecas y aplicaciones más livianas, rápidas
y menos complicadas. Dado que este enfoque puede utilizar declaraciones explícitas de
importación y exportación, es más efectivo que simplemente ejecutar un minificador
automático para detectar variables no utilizadas en el código de salida compilado.
import foo from './foo.js';
export default function() {
console.log(foo);
}
Figura 3.5: Archivo de ejemplo de main.js
export default 'hello world!';
Figura 3.6: Archivo de ejemplo foo.js
A modo de ejemplo se utilizaran dos archivos JavaScript, main.js (Figura 3.5) y foo.js
(Figura 3.6). Se ejecuta la línea de comandos: $rollup src/main.js -o bundle.js -f cjs. La
opción -f (--format) especifica qué tipo de paquete se está creando, en este caso,
CommonJS (que se ejecutará en Node.js). Y se especifica el archivo de salida bundle.js
(Figura 3.7).
'use strict';
const foo = 'hello world!';
const main = function() {
console.log(foo);
};
module.exports = main;
Figura 3.7: Ejemplo de bundle.js usando Rollup
Si bien esta técnica tiene la ventaja de no importar bibliotecas que no son utilizadas
en el código, tiene como desventaja que si se usan pocas funciones de una determinada
biblioteca, la técnica incluirá en el bundle la totalidad de la misma. En este sentido soluciona
parcialmente los problemas que tienen Webpack y Browserify, pero en algunos casos, el
bundle seguirá siendo muy grande.
3.1.7 UFFRemover
Otra herramienta de optimización de código es UFFRemover [5], una característica
clave es que ayuda a obtener información sobre las ejecuciones de funciones en tiempo de
ejecución. Esta información permite eliminar funciones no utilizadas independientemente de
si se utilizan los módulos, lo que produce reducciones en el tamaño final de la aplicación. A
37
diferencia del código muerto y de las funciones inalcanzables, estas funciones están
contenidas en una biblioteca JavaScript utilizada por una aplicacióndeterminada, que no
son utilizadas por la aplicación pero, que de todas formas, son empaquetadas con el resto
del código necesario. Dichas funciones son definidas como UFF (Unused Foreign Function).
Con el objetivo de identificar y remover las UFFs se realiza un análisis estático y dinámico
del código de la aplicación y sus dependencias. Con esta herramienta se obtiene una
mejora importante en la reducción de tamaño de los bundles JavaScript de alrededor del
25% del código de las aplicaciones.
Figura 3.8: Resumen del tratamiento de UFF
La eliminación UFF contribuye a reducir el tamaño final de los archivos de
distribución sin afectar el comportamiento de la aplicación. Una visión general del enfoque
se muestra en la Figura 3.8, este tiene dos etapas: (A) la identificación de UFF y (B) la
eliminación de UFF. La etapa de identificación UFF consiste en determinar si una función de
biblioteca es innecesaria en el contexto de una aplicación. Esta etapa consta de 3
actividades: identificación de módulos requeridos, instrumentación de módulos requeridos y
detección dinámica de UFF. La primera actividad es un análisis estático del código de la
aplicación para identificar los módulos requeridos. Se utiliza la herramienta Browserify para
analizar las dependencias entre módulos descartando aquellos que no son requeridos.
Luego, en la actividad de instrumentación de módulos requeridos, se instrumentan todas las
38
funciones de los módulos que no fueron descartados en la actividad anterior con el objetivo
de recolectar información sobre las funciones que efectivamente serán ejecutadas. El
proceso de instrumentación identifica las funciones en los archivos JavaScript basándose
en el estándar ES5. Se agrega una instrucción en cada función para registrar durante la
ejecución cuales son utilizadas. Para generar esta información el programa debe ser
utilizado. Esta tarea se puede realizar ejecutando tests o interactuando con el programa en
producción. La última actividad se encarga de clasificar como UFFs las funciones que no
fueron utilizadas.
A pesar de las virtudes de UFFRemover relacionado a la optimización, tiene algunos
problemas de diseño. Uno de ellos es el hecho de que una de las formas de optimización
está basada en casos de test de la aplicación objetivo, esto produce problemas porque la
calidad de la optimización va a depender del esfuerzo que el desarrollador haga en la
creación del archivo instrumentado. Por otro lado, al ser una herramienta batch se debe
ejecutar cada vez que se necesite adquirir nueva información de la aplicación.
3.2 Análisis Comparativo
En esta sección se analizarán las diferencias fundamentales encontradas entre las
herramientas de optimización de código Js descritas en la sección anterior.
En la Tabla 3.9 se presenta una comparación entre las distintas herramientas
describiendo si cumplen o no con determinadas condiciones. Se puede observar que la
mayoría de las herramientas realizan solo un análisis estático, sólo analizan el código
durante la escritura o compilando el mismo, no durante la ejecución de la aplicación.
Estático Dinámico Detecta
errores
Mejora
performance
Elimina código
sin uso
Lazy Propagation
Modelado del HTML
DOM y Browser API
JSNose
Browserify
Webpack
Rollup
UFF Remover
Tabla 3.9: Comparación de características de las distintas aplicaciones
39
En cuanto a la detección de errores de sintaxis, esto solo es realizado por las
herramientas de análisis estático que no provocan una mejora en la performance de la
aplicación. Sólo se analiza que esté correctamente escrito el código sugiriendo posibles
correcciones. JSNose va un poco más allá, detectando también posibles errores de diseño,
buscando code smells en forma dinámica también.
Las herramientas que se enfocan en la mejora de performance, en su mayoría
realizan un análisis estático del código tratando de reducir el tamaño y/o la cantidad de los
archivos. Como resultado de esto, se produce una reducción en los tiempos de descarga.
Solo UFFRemover realiza un análisis dinámico.
Sobre la optimización de la eliminación de código sin uso es realizado por dos de las
herramientas. Rollup que analiza estáticamente el código que está importando y excluirá
todo lo que no se use realmente. UFFRemover remueve el código no utilizado detectando
durante la ejecución de la aplicación cuáles son las funciones que se usan y cuáles no.
3.3 Resumen
A lo largo de este capítulo, se han presentado los trabajos más relevantes en el área
de la optimización de desarrollos JavaScript. Sin embargo, la mayoría de las herramientas
no realizan un análisis dinámico de la aplicación y las que sí lo hacen tienen muchas
limitaciones. En primer lugar, hay que destacar que todas estas herramientas realizan la
optimización en la etapa de desarrollo de la aplicación, provocando que cada optimización
produzca una nueva versión de la aplicación. Además, en el caso de JSNose el análisis
dinámico solo está orientado a encontrar code smells, y con respecto a UFFRemover, se
identificaron tres limitaciones importantes, que se discuten a continuación.
La primera está relacionada con la forma en que UFFRemover realiza el análisis
dinámico de las aplicaciones. Dado que una de las posibilidades de UFFRemover es
basarse en casos de test proporcionados por las aplicaciones JavaScript, para un buen
funcionamiento se requiere una alta cobertura de test de las funciones (superior al 85%).
Sin embargo, en la práctica, es muy posible que las aplicaciones JavaScript no lleguen a
tener la cobertura necesaria, ya sea porque existe funcionalidad muy difícil de probar en
tiempo de desarrollo (por ejemplo, casos de concurrencia de usuarios o comunicación con
otros servicios difíciles de simular), o porque los desarrolladores no tienen el tiempo
necesario para proporcionar la cobertura necesaria. Esto es contraproducente para el buen
funcionamiento de UFFRemover ya que una baja cobertura de test generaría muchos
“falsos positivos”, y produciría consecuentemente muchas llamadas al servidor para cargar
UFFs, lo cual afectaría negativamente la performance de las aplicaciones.
Pese a haber sido desarrollado para recopilar información utilizando los test, es
posible instrumentar el código directamente en producción para recopilar información de los
usuarios. Sin embargo, en este punto surge otra limitación, UFFRemover es una
herramienta "batch" en el sentido que tiene que ser ejecutada por los desarrolladores cada
vez que se tiene nueva información de los usuarios. Debido a esto, los usuarios tienen que
40
tener el código instrumentado constantemente en producción para poder seguir generando
trazas de ejecución y así mantener los archivos JavaScript optimizados de manera
actualizada. Mantener instrumentados los archivos en producción es costoso, ya que la
instrumentación introduce código en las aplicaciones y genera un archivo JavaScript mucho
más grande que el original.
Finalmente, la tercera limitación es que la herramienta no proporciona una estrategia
en caso de que haya funciones que fueron detectadas como falsos positivos (funciones que
fueron detectadas como UFFs y no lo eran). En caso de que se optimice un archivo
JavaScript y se eliminen funciones que en realidad si son utilizadas por la aplicación web,
generaría que el código no funcione como se espera, produciendo bugs no deseados
durante su uso, con todos los peligros que esto conlleva.
41
Capítulo 4: UFFR Extension
La herramienta UFF Remover provee mecanismos para identificar las funciones25
que son utilizadas por una página web, realizando un análisis dinámico de sus archivos
JavaScript (JS) mientras se hace uso de la misma. Con esta información es posible realizar
una optimización, eliminando las funciones que no fueron utilizadas, lo cual permite obtener
archivos optimizados de un menor tamaño que los originales.
A pesar de los beneficios que provee UFF Remover en la optimización de los
archivos JavaScript, no analiza la posible existencia de falsos positivos, es decir,

Continuar navegando