Cómo mejorar el rendimiento de tu aplicación Angular

Publicado: 2020-04-10

Cuando se habla de los mejores marcos frontend, es imposible no mencionar Angular. Sin embargo, requiere mucho esfuerzo de los programadores aprenderlo y usarlo sabiamente. Desafortunadamente, existe el riesgo de que los desarrolladores que no tienen experiencia en Angular puedan usar algunas de sus funciones de manera ineficiente.

Una de las muchas cosas en las que siempre debe trabajar como desarrollador frontend es el rendimiento de la aplicación. Una gran parte de mis proyectos anteriores se centró en grandes aplicaciones empresariales que continúan expandiéndose y desarrollándose. Los marcos frontend serían extremadamente útiles aquí, pero es importante usarlos de manera correcta y razonable.

He preparado una lista rápida de las estrategias y consejos de aumento de rendimiento más populares que pueden ayudarlo a aumentar instantáneamente el rendimiento de su aplicación Angular . Tenga en cuenta que todas las sugerencias aquí se aplican a Angular en la versión 8.

ChangeDetectionStrategy y ChangeDetectorRef

Change Detection (CD) es el mecanismo de Angular para detectar cambios en los datos y reaccionar automáticamente ante ellos. Podemos enumerar el tipo básico de cambios de estado de aplicación estándar:

  • Eventos
  • Solicitud HTTP
  • Temporizadores

Estas son interacciones asincrónicas. La pregunta es: ¿cómo sabría Angular que se produjeron algunas interacciones (como clic, intervalo, solicitud http) y que es necesario actualizar el estado de la aplicación?

La respuesta es ngZone , que es básicamente un sistema complejo destinado a rastrear interacciones asincrónicas. Si todas las operaciones están registradas por ngZone, Angular sabe cuándo reaccionar ante algunos cambios. Pero no sabe qué ha cambiado exactamente y lanza el mecanismo de detección de cambios , que verifica todos los componentes en primer orden de profundidad.

Cada componente de la aplicación Angular tiene su propio detector de cambios, que define cómo debe actuar este componente cuando se inicia la detección de cambios; por ejemplo, si es necesario volver a generar el DOM de un componente (lo cual es una operación bastante costosa). Cuando Angular inicia la Detección de cambios, se comprobará cada uno de los componentes y su vista (DOM) se volverá a representar de forma predeterminada.

Podemos evitar esto usando ChangeDetectionStrategy.OnPush:

 @Componente({
  selector: 'foobar',
  templateUrl: './foobar.component.html',
  URL de estilo: ['./foobar.component.scss'],
  detección de cambios: ChangeDetectionStrategy.OnPush
})

Como puede ver en el código de ejemplo anterior, tenemos que agregar un parámetro adicional al decorador del componente. Pero, ¿cómo funciona realmente esta nueva estrategia de detección de cambios?

La estrategia le dice a Angular que un componente específico solo depende de sus @Inputs(). Además, todos los componentes @Inputs() actuarán como un objeto inmutable (por ejemplo, cuando cambiamos solo la propiedad en el @Input() de un objeto, sin cambiar la referencia, este componente no se verificará). Significa que se omitirán muchas comprobaciones innecesarias y debería aumentar el rendimiento de nuestra aplicación.

Un componente con ChangeDetectionStrategy.OnPush se verificará solo en los siguientes casos:

  • La referencia @Input() cambiará
  • Se activará un evento en la plantilla del componente o en uno de sus hijos.
  • Observable en el componente activará un evento
  • El CD se ejecutará manualmente usando el servicio ChangeDetectorRef
  • la tubería asíncrona se usa en la vista (la tubería asíncrona marca el componente que se verificará en busca de cambios; cuando el flujo de origen emita un nuevo valor, este componente se verificará)

Si no sucede nada de lo anterior, el uso de ChangeDetectionStrategy.OnPush dentro de un componente específico hace que el componente y todos los componentes anidados no se verifiquen después del lanzamiento del CD.

Afortunadamente, aún podemos tener control total para reaccionar a los cambios de datos mediante el servicio ChangeDetectorRef. Debemos recordar que con ChangeDetectionStrategy.OnPush dentro de nuestros tiempos de espera, solicitudes, devoluciones de llamada de suscripciones, debemos activar el CD manualmente si realmente necesitamos esto:

 contador = 0;

constructor(privado changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  establecerTiempo de espera(() => {
    este.contador += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

Como podemos ver arriba, al llamar a this.changeDetectorRef.detectChanges() dentro de nuestra función de tiempo de espera , podemos forzar el CD manualmente. Si el contador se usa dentro de la plantilla de alguna manera, su valor se actualizará.

El último consejo de esta sección trata sobre la desactivación permanente de CD para componentes específicos. Si tenemos un componente estático y estamos seguros de que no se debe cambiar su estado, podemos deshabilitar el CD de forma permanente:

 this.changeDetectorRef.detach()

Este código debe ejecutarse dentro del método de ciclo de vida ngAfterViewInit() o ngAfterViewChecked(), para asegurarnos de que nuestra vista se procesó correctamente antes de deshabilitar la actualización de datos. Este componente ya no se verificará durante CD , a menos que activemos detectChanges() manualmente.

Llamadas a funciones y captadores en plantilla

El uso de llamadas a funciones dentro de las plantillas ejecuta esta función cada vez que se ejecuta el detector de cambios. La misma situación ocurre con getters . Si es posible, debemos tratar de evitar esto. En la mayoría de los casos, no necesitamos ejecutar ninguna función dentro de la plantilla del componente durante cada ejecución del CD . En lugar de eso, podemos usar tuberías puras.

pipas puras

Los conductos puros son un tipo de conductos con una salida que depende únicamente de su entrada, sin efectos secundarios. Afortunadamente, todas las tuberías en Angular son puras por defecto.

 @Tubo({
    nombre: 'mayúsculas',
    puro: verdadero
})

Pero, ¿por qué deberíamos evitar usar tuberías con puro: falso? La respuesta es Detección de cambios nuevamente. Pipes que no son puros se ejecutan en cada ejecución de CD, lo que no es necesario en la mayoría de los casos y disminuye el rendimiento de nuestra aplicación. Aquí está el ejemplo de la función que podemos cambiar a tubería pura:

 transform(valor: cadena, límite = 60, puntos suspensivos = '...') {
  if (!valor || valor.longitud <= límite) {
    valor de retorno;
  }
  const numeroDeCaracteresVisibles = valor.substr(0, limite).lastIndexOf(' ');
  return `${value.substr(0, numberOfVisibleCharacters)}${elipsis}`;
}

Y veamos la vista:

 <p class="description">truncar(texto, 30)</p>

El código anterior representa la función pura: sin efectos secundarios, la salida solo depende de las entradas. En este caso, podemos simplemente reemplazar esta función por tubería pura :

 @Tubo({
  nombre: 'truncar',
  puro: verdadero
})
clase de exportación TruncatePipe implementa PipeTransform {
  transform(valor: cadena, límite = 60, puntos suspensivos = '...') {
    ...
  }
}

Y finalmente, en esta vista, obtenemos el código, que se ejecutará solo cuando se modifique el texto, independientemente de la detección de cambios .

 <p class="descripción">{{ texto | truncar: 30 }}</p>

Módulos de precarga y carga diferida

Cuando su aplicación tiene más de una página, definitivamente debería considerar crear módulos para cada pieza lógica de su proyecto, especialmente módulos de carga diferida . Consideremos el código de enrutador angular simple:

 const rutas: Rutas = [
  {
    sendero: '',
    componente: Componente de inicio
  },
  {
    camino: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    ruta: 'barra',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@MóduloNg({
  exportaciones: [RouterModule],
  importaciones: [RouterModule.forRoot(rutas)]
})
clase AppRoutingModule {}

En el ejemplo anterior, podemos ver que fooModule con todos sus activos se cargará solo cuando el usuario intente ingresar a una ruta específica (foo o bar). Angular también generará un fragmento separado para este módulo . Lazy loading reducirá la carga inicial .

Podemos hacer alguna optimización adicional. Supongamos que queremos que nuestra aplicación cargue módulos en segundo plano. Para este caso, podemos usar la estrategia de precarga. Angular por defecto tiene dos tipos de preloadingStrategy:

  • Sin precarga
  • PreloadAllModules

En el código anterior, la estrategia NoPreloading se usa de forma predeterminada. La aplicación comienza a cargar un módulo específico por solicitud del usuario (cuando el usuario quiere ver una ruta específica). Podemos cambiar esto agregando alguna configuración adicional al enrutador.

 @MóduloNg({
  exportaciones: [RouterModule],
  importaciones: [RouterModule.forRoot(rutas, {
       preloadingStrategy: PreloadAllModules
  }]
})
clase AppRoutingModule {}

Esta configuración hace que la ruta actual se muestre lo antes posible y luego la aplicación intentará cargar los otros módulos en segundo plano. Inteligente, ¿no? Pero eso no es todo. Si esta solución no se ajusta a nuestras necesidades, simplemente podemos escribir nuestra propia estrategia personalizada .

Supongamos que queremos precargar solo módulos seleccionados, por ejemplo, BarModule. Indicamos esto agregando un campo adicional para el campo de datos.

 const rutas: Rutas = [
  {
    sendero: '',
    componente: Componente de inicio
    datos: {precarga: falso}
  },
  {
    camino: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    datos: {precarga: falso}
  },
  {
    ruta: 'barra',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    datos: {precarga: verdadero}
  }
]

Luego tenemos que escribir nuestra función de precarga personalizada:

 @Inyectable()
clase de exportación CustomPreloadingStrategy implementa PreloadingStrategy {
  preload(ruta: Ruta, carga: () => Observable<cualquiera>): Observable<cualquiera> {
    volver ruta.datos && ruta.datos.precarga ? carga (): de (nulo);
  }
}

Y configúralo como una estrategia de precarga:

 @MóduloNg({
  exportaciones: [RouterModule],
  importaciones: [RouterModule.forRoot(rutas, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
clase AppRoutingModule {}

Por ahora solo se precargarán las rutas con param {datos: {preload: true}}. El resto de las rutas actuarán como si NoPreloading estuviera configurado.

La estrategia de carga previa personalizada es @Injectable(), por lo que significa que podemos inyectar algunos servicios dentro si es necesario y personalizar nuestra estrategia de carga previa de cualquier otra manera.
Con las herramientas de desarrollo de un navegador, podemos investigar el aumento del rendimiento mediante el mismo tiempo de carga inicial con y sin una estrategia de precarga. También podemos mirar la pestaña de red para ver que los fragmentos de otras rutas se están cargando en segundo plano, mientras que el usuario puede ver la página actual sin demoras.

función trackBy

Podemos suponer que la mayoría de las aplicaciones de Angular usan *ngFor para iterar sobre los elementos enumerados dentro de la plantilla. Si la lista iterada también es editable, trackBy es absolutamente imprescindible.

 <ul>
  <tr *ngFor="let producto de productos; trackBy: trackByProductId">
    <td>{{ producto.título }}</td>
  </tr>
</ul>

trackByProductId(índice: número, producto: Producto) {
  volver producto.id;
}

Al usar la función trackBy, Angular puede rastrear qué elementos de las colecciones han cambiado (por identificador dado) y volver a representar solo estos elementos en particular. Cuando omitimos trackBy, la lista completa se volverá a cargar, lo que puede ser una operación que consume muchos recursos en DOM.

Compilación anticipada (AOT)

Con respecto a la documentación angular:

(…) los componentes y plantillas proporcionados por Angular no pueden ser entendidos directamente por el navegador, las aplicaciones de Angular requieren un proceso de compilación antes de que puedan ejecutarse en un navegador

Angular proporciona los dos tipos de compilación:

  • Just-in-Time (JIT): compila una aplicación en el navegador en tiempo de ejecución
  • Ahead-of-Time (AOT): compila una aplicación en el momento de la compilación

Para el uso de desarrollo, la compilación JIT debe cubrir las necesidades del desarrollador. Sin embargo, para la construcción de producción definitivamente deberíamos usar el AOT . Necesitamos asegurarnos de que el indicador aot dentro del archivo angular.json esté establecido en verdadero. Los beneficios más importantes de una solución de este tipo incluyen una representación más rápida, menos solicitudes asincrónicas, un tamaño de descarga de marco más pequeño y una mayor seguridad.

Resumen

El rendimiento de la aplicación es algo que debe tener en cuenta tanto durante el desarrollo como durante la parte de mantenimiento de su proyecto. Sin embargo, buscar posibles soluciones por su cuenta puede llevar mucho tiempo y esfuerzo. Verificar estos errores comunes y tenerlos en cuenta durante el proceso de desarrollo no solo lo ayudará a mejorar el rendimiento de su aplicación Angular en poco tiempo, sino que también lo ayudará a evitar fallas futuras.

Liberar icono de producto

Confía en Miquido para tu proyecto Angular

Contáctenos

¿Quieres desarrollar una app con Miquido?

¿Estás pensando en darle un impulso a tu negocio con una aplicación Angular? Ponte en contacto con nosotros y elige nuestros servicios de desarrollo de aplicaciones Angular.