Mejorar el desempeño en aplicaciones Angular

Mejorar el desempeño en aplicaciones Angular

Álvaro Junqueira. Después de todo, ¿qué es el desempeño o performance? ¿Qué hace que un usuario se dé cuenta de que una aplicación está funcionando bien o no?

Cuenta la leyenda que solo ponen un espejo dentro de los ascensores para que se vea más rápido, porque  quien entra al ascensor se distrae con él y el tiempo parece pasar más rápido.

A eso se le llama rendimiento percibido. A veces quitando un loading que bloquea toda una pantalla y colocando el loading directamente dentro de una tabla cargando los datos hace parecer que todo funcionó más rápido, porque el usuario ya podía cambiar otras funcionalidades, pero la verdad es que los datos se cargaron al mismo tiempo.

Cuando hablamos de rendimiento para aplicaciones web, es un concepto muy amplio, por lo que aquí nos vamos a centrar en cómo mejorar el rendimiento de tu aplicación en tres puntos principales:

- Carga de la aplicación.
- Ejecución.
- Memoria.

Desempeño o performance para cargar la aplicación


Cargar una aplicación SPA como Angular siempre es un reto. Existen varios archivos JavaScript y varias técnicas que se pueden aplicar. El propio framework Angular ya nos ofrece algunas formas de mejorar la forma en la que cargamos la aplicación en el navegador. En lo que nos vamos a centrar aquí entonces es en el Time-to-Interact, que dicta cuánto tiempo tardará el usuario en escribir la URL para entrar en la aplicación hasta que pueda realizar la primera interacción.

Ivy compiler

El consejo aquí prácticamente es: ten la versión de Angular actualizada. Desde la versión 9 de Angular, el equipo ha estado cambiando el compilador principal llamado View Engine al nuevo Ivy, que es muy recomendable no deshabilitarlo por mucho que se pueda hacerlo manualmente.

El nuevo compilador ofrece un tree shaking mucho más optimizada para tu aplicación y debe usarse junto con AOT (aot=true en tu angular.json). Para obtener ayuda sobre cómo actualizar tu aplicación Angular, revisa la herramienta oficial.

Brotli

Brotli es un compresor de datos de código abierto de Google y ya es compatible con la mayoría de los navegadores:

Brotli se puede usar para comprimir las respuestas HTTPS enviadas al navegador en lugar de gzip o deflate. Esta configuración debe hacerse en tu WebServer y algunos de ellos ya tienen esta configuración habilitada.

Para saber si tu aplicación ya tiene Brotli en el servidor, abre la aplicación, ingresa a DevTools y buscala llamada de JavaScript. El sitio web caniuse.com en sí no tiene activado Brotli, solo gzip:

Los JavaScripts de la página de documentación angular.io mostrarán el br (Brotli):

Una aplicación puede tener configuradas varias compresiones mientras que el WebServer siempre entregará el archivo más pequeño y comprimido.

GZIPO Gzip es el archivador predeterminado. Todos los WebServers ya cuentan con una forma sencilla de configurar y personalizar para empaquetar solo lo que necesitas. Además de los archivos Javascript de la aplicación, Gzip puede y debe usarse para comprimir todas las respuestas HTTP, por lo que tu backend debe estar configurado para entregar respuestas API ya con Gzip activado (fácil de configurar en SpringBoot o .Net, por ejemplo).

La configuración para una aplicación Angular es que los activos deben configurarse según los siguientes tipos de MIME:

  • application/json.
  • text/html.
  • text/css.

Y las principales personalizaciones importantes que se pueden realizar son:

● Tipos MIME a comprimir (text/html, application/json etc.).

● Tamaño mínimo de archivo que el WebServer considerará para comprimir o no un determinado archivo, normalmente 1024 bytes. Si el WebServer intenta comprimir un archivo más pequeño que éste, es posible que sea más grande que el archivo sin comprimir.

GZIP estático: algunos WebServers tienen una forma de configurarlo para comprimir un archivo solo la primera vez que se llama y luego almacenarlo en el servidor para ser entregado en las próximas solicitudes, ahorrando tiempo sin tener que comprimir cada vez.

Lazy Load

De forma predeterminada, los módulos de Angular que se importan directamente en el bloque de imports de AppModule de Angular se incluirán en el archivo principal de JavaScript (main.js), ejemplo:

Sin embargo, para aplicaciones grandes, es posible que no desees cargar todos los módulos y funciones al iniciar la aplicación.

Para esto, debes cambiar la forma en que se cargará este módulo para que se cargue solo cuando se llame a una determinada ruta, así que elimínalo de las imports y muévelo a tu archivo de rutas usando el método loadChildren para hacerlo:

De esta manera, el módulo MyModule y todos los componentes declarados en él no estarán dentro del main.js (se creará un js solo para él) y este JavaScript solo se llamará en el navegador al acceder a la ruta myroute.

Consejo: Nunca configures Lazy Load para la ruta principal.

Supongamos que tiene la siguiente configuración de ruta:

Al acceder a tu aplicación en la primera ruta (https://example.com/), tu aplicación te redirigirá a la ruta del tablero, haciendo que el navegador aún habiendo descargado el archivo principal para el primer render (main.js) tenga esperar una descarga más (mymodule.js) solo para mostrar el primer contenido. Nunca pongas la primera ruta para llamar en Lazy Load, ya que esto afectará directamente la carga de tu aplicación.

Lo último que puedes hacer con Lazy Load es precargar las rutas después del primer renderizado. Esto hará que la experiencia del usuario al usar la aplicación sea más rápida, ya que todas las demás rutas visibles ya estarán listas para usar en el navegador del usuario (esto no impacta la primera carga, solo mejorará el rendimiento al cargar los demás módulos).

Para realizar esta configuración, puedes usar la herramienta ngx-quicklinks: https://github.com/mgechev/ngx-quicklink

Cache


Una práctica muy común es el almacenamiento en caché de archivos estáticos. El navegador en sí colocará, de manera predeterminada, los archivos de tu aplicación (js, css, html, fuentes, imágenes, etc.) en el caché local, pero hay varias formas de controlar el caché de archivos.

Algunas formas con las que puedes trabajar mejor con el almacenamiento en caché:

Service Workers: con ellos puede incluir en el caché no solo archivos sino también resultados de API.

Caché a nivel de servidor web: agregar y trabajar mejor con el encabezado Cache-Control.

Caché a nivel del navegador: administrar lo que se desea o no incluir en el almacenamiento de caché del navegador. Consulta la API aquí.

Performance de la aplicación en ejecución

De acuerdo, la aplicación ya se carga rápido, pero cuando el usuario hace clic en un botón para realizar alguna interacción, todavía parece lento. ¿Cómo podemos mejorar eso?

enableProdMode()

Algo que debemos verificar es si se llama al método enableProdMode() en el archivo main.ts. Sin este método, Angular ejecutará varias Change Detections para ayudarte en el desarrollo, llamar a este método en Dev solo dificultará tu desarrollo, así que verifica que en tu archivo el método solo se llame para producción:


Web Workers

Un problema común en las aplicaciones SPA es que todo el JavaScript se ejecuta en el hilo único del navegador. En aplicaciones complejas con un gran árbol de componentes donde la detección de cambios necesita ejecutar miles de comprobaciones cada segundo, el rendimiento comenzará a disminuir fácilmente.

Los Web Workers funcionan como un nuevo thread en el browser. Puedes exigir procesos lentos y eso puede bloquear el hilo principal. Asimismo, puedes elegir, por ejemplo, enviar un Web Worker para ejecutar parte de tu aplicación y dejar que el principal solo represente el DOM o pedirle que genere un elemento para ti, como un archivo de Excel, por ejemplo.

Esto te permitiría dejar el navegador libre para ejecutar la parte principal de tu aplicación sin dejar la pantalla en blanco mientras procesas algo. Para aprender a crear un Web Worker en Angular: https://angular.io/guide/web-worker.

Change Detection

Con cada evento asíncrono, Angular realizará una detección de cambios en todo el árbol de componentes, por ejemplo:

En la imagen de arriba, si se cambia el componente Parent, todo el árbol detectará el cambio, ya que son elementos secundarios de Parent.

Con esto, todos los componentes se representarán nuevamente en el DOM y vincularán todas las variables también.

Esto se llama Change Detection Strategy en Angular y este es el comportamiento predeterminado que ayuda mucho para que no tengamos que preguntarnos si queremos o no actualizar el árbol de componentes. Pero digamos que en este árbol el componente Comp 3 es demasiado costoso y engorroso para actualizarlo con cada cambio en tu árbol.

Lo que puedes hacer es cambiar manualmente este componente para que se actualice solo cuando lo necesites. Para deshabilitar el Change Detection del componente de forma predeterminada y deberás cambiarlo también en OnPush:

La configuración de OnPush ahora dice que va a controlar la representación de este componente, por lo que, dentro de la lógica que tiene, puede activar la representación directamente (hay varias formas, como llamar al método tick() de ComponentRef, por ejemplo).

trackBy (opción de *ngFor)

De forma predeterminada, *ngFor identifica la singularidad de un objeto por su referencia, por lo que cuando el desarrollador agrega un elemento a la lista mediante programación sobre la que ngFor está iterando, Angular no puede saber exactamente cuál elemento se agregó al array, por lo que simplemente elimina todos los elementos del DOM y agrega la lista actual con el elemento incluido.

Para evitar esto, puedes decirle a Angular cómo indexar tu lista de objetos, lo que permite que Angular solo represente lo que ha cambiado. Tu sintaxis ngFor se vería así:

Y el Typescript así:

Esta técnica es perfecta cuando tienes una lista en tu HTML que se actualiza manualmente en el frontend, no cuando todos los elementos provienen de una API y los vas a agregar todos a la vez.

Para obtener más información, consulta: https://angular.io/guide/directivas-incorporadas#ngfor-with-trackby

Performance de memoria


Por último, pero no menos importante, el rendimiento de la máquina (o teléfono celular) del usuario. A medida que usas tu aplicación, ¿te da la impresión de que el navegador o la máquina misma se están volviendo cada vez más lentos?

Puede ser que tu aplicación esté usando cada vez más memoria y ni siquiera lo sepas (cualquiera que haya visto Chrome usando prácticamente toda la memoria de una máquina sabe de lo que hablo).

Aquí te doy dos consejos interesantes: cancela la suscripción a tus Observables y evita usar el addEventListener de JavaScript.

Unsubscribe en los Observables

Cuando haces un .subscribe() en un Observable/Subject/BehaviorSubject, creas una suscripción en la memoria, que se almacena allí para notificarte siempre cuando ocurre un evento y transmitir el mensaje.

Sin embargo, después de cambiar de un componente a otro en el navegador, Angular destruye solo el componente, pero no las suscripciones de memoria.

Tienes que hacer esto manualmente: cada vez que se destruya un componente, elimina las suscripciones que había en él. Hay varias formas de hacerlo. Las principales formas, usando RxJs, son con:

● takeUntil – la más recomendada.
● takeWhile.
● unsubscribe.

Nota: El pipe AsyncPipe también se suscribe directamente al HTML. Sin embargo, cuando se destruye un componente, se da de baja automáticamente =). El método takeUntil es el método más recomendado para mantener un código limpio y fácil de replicar. Lo único que tendrás que hacer es incluir en tus subscribes:

Puede ver que arriba solo necesitabas una variable para almacenar la referencia de todas las suscripciones para simplemente llamarla en el ngOnDestroy del componente y completarla, eliminando así todas las suscripciones en la memoria.

Evita usar addEventListener

Siempre que crees un listener en el navegador con addEventListener, existirá hasta que llames a removeEventListener, sigue la misma lógica que las subscriptions de RxJs.

Si creas los listeners usando las API de Angular, se pueden eliminar automáticamente de la memoria cuando los destruyas.

Hay varias formas de usar Angular para crear un listener, estas son:

EventBinding - técnica que coloca al listener directamente en el HTML, como por ejemplo:

o:

HostListener - La forma más sencilla que también eliminará el listener una vez que se destruya el componente, por ejemplo:

Renderer2 - La clase Renderer2 ofrece un método llamado listen que devuelve una función que, si se invoca, elimina el listener de la memoria. Es muy importante cuando tu interacción depende de la inserción y eliminación de listeners en tiempo real, no solo cuando se destruye un componente que se arrastra.

Conclusión


Al final de este artículo, su aplicación probablemente volará =o.

Hay muchas, muchas técnicas que aún se pueden aplicar. Las enumeradas aquí son las más importantes cuando hablamos de aplicaciones Angular (algunas de las cuales incluso se pueden usar para otras tecnologías SPA como Vue o React). Así que recuérdalas en tu próxima revisión de código o cuando crees tu próxima aplicación web =).

Visita mi GitHub: https://github.com/alvarocjunq

¡Hasta luego!

⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.