Descarga la aplicación para disfrutar aún más
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,
Compartir