Optimiza la detección de cambios de Angular

Implementa una detección de cambios más rápida para mejorar la experiencia del usuario.

Angular ejecuta su mecanismo de detección de cambios periódicamente para que los cambios en el modelo de datos se reflejen en la vista de una app. La detección de cambios se puede activar de forma manual o a través de un evento asíncrono (por ejemplo, una interacción del usuario o una finalización de XHR).

La detección de cambios es una herramienta potente, pero si se ejecuta con mucha frecuencia, puede activar muchos cálculos y bloquear el subproceso principal del navegador.

En esta publicación, aprenderás a controlar y optimizar el mecanismo de detección de cambios omitiendo partes de tu aplicación y ejecutando la detección de cambios solo cuando sea necesario.

Detección de cambios de Angular

Para comprender cómo funciona la detección de cambios de Angular, veamos una app de ejemplo.

Puedes encontrar el código de la app en este repositorio de GitHub.

La aplicación muestra a los empleados de dos departamentos de una empresa: ventas y I+D, y tiene dos componentes:

  • AppComponent, que es el componente raíz de la app
  • Dos instancias de EmployeeListComponent, una para ventas y otra para I+D.

Aplicación de ejemplo

Puedes ver las dos instancias de EmployeeListComponent en la plantilla de AppComponent:

<app-employee-list
  [
data]="salesList"
 
department="Sales"
  (
add)="add(salesList, $event)"
  (
remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [
data]="rndList"
 
department="R&D"
  (
add)="add(rndList, $event)"
  (
remove)="remove(rndList, $event)"
></app-employee-list>

Para cada empleado, hay un nombre y un valor numérico. La app pasa el valor numérico del empleado a un cálculo empresarial y visualiza el resultado en la pantalla.

Ahora, observa EmployeeListComponent:

const fibonacci = (num: number): number => {
 
if (num === 1 || num === 2) {
   
return 1;
 
}
 
return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
 
@Input() data: EmployeeData[];
 
@Input() department: string;
 
@Output() remove = new EventEmitter<EmployeeData>();
 
@Output() add = new EventEmitter<string>();

  label
: string;

  handleKey
(event: any) {
   
if (event.keyCode === 13) {
     
this.add.emit(this.label);
     
this.label = '';
   
}
 
}

  calculate
(num: number) {
   
return fibonacci(num);
 
}
}

EmployeeListComponent acepta una lista de empleados y un nombre de departamento como entradas. Cuando el usuario intenta quitar o agregar un empleado, el componente activa un resultado correspondiente. El componente también define el método calculate, que implementa el cálculo empresarial.

Esta es la plantilla para EmployeeListComponent:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
 
<input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
 
<mat-list-item *ngFor="let item of data">
   
<h3 matLine title="Name">
      {{ item.label }}
   
</h3>
   
<md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
   
</md-chip>
 
</mat-list-item>
</mat-list>

Este código itera en todos los empleados de la lista y, para cada uno, renderiza un elemento de lista. También incluye una directiva ngModel para la vinculación de datos bidireccional entre la entrada y la propiedad label declarada en EmployeeListComponent.

Con las dos instancias de EmployeeListComponent, la app forma el siguiente árbol de componentes:

Árbol de componentes

AppComponent es el componente raíz de la aplicación. Sus componentes secundarios son las dos instancias de EmployeeListComponent. Cada instancia tiene una lista de elementos (E1, E2, etc.) que representan a los empleados individuales del departamento.

Cuando el usuario comienza a ingresar el nombre de un empleado nuevo en el cuadro de entrada de unEmployeeListComponent, Angular activa la detección de cambios para todo el árbol de componentes a partir de AppComponent. Esto significa que, mientras el usuario escribe en la entrada de texto, Angular vuelve a calcular repetidamente los valores numéricos asociados con cada empleado para verificar que no hayan cambiado desde la última verificación.

Para ver qué tan lento puede ser esto, abre la versión no optimizada del proyecto en StackBlitz y prueba ingresar el nombre de un empleado.

Para verificar que la ralentización proviene de la función fibonacci, configura el proyecto de ejemplo y abre la pestaña Rendimiento de Chrome DevTools.

  1. Presiona "Control + Mayúsculas + J" (o "Comando + Opción + J" en Mac) para abrir DevTools.
  2. Haz clic en la pestaña Rendimiento.

Ahora, haz clic en Grabar (en la esquina superior izquierda del panel Rendimiento) y comienza a escribir en uno de los cuadros de texto de la app. En unos segundos, vuelve a hacer clic en Grabar para detener la grabación. Una vez que Chrome DevTools procese todos los datos de perfil que recopiló, verás algo como lo siguiente:

Generación de perfiles de rendimiento

Si hay muchos empleados en la lista, este proceso puede bloquear el subproceso de IU del navegador y provocar que se pierdan fotogramas, lo que genera una mala experiencia del usuario.

Omisión de subárboles de componentes

Cuando el usuario escribe la entrada de texto para el EmployeeListComponent ventas, sabes que los datos del departamento de I+D no cambian, por lo que no hay razón para ejecutar la detección de cambios en su componente. Para asegurarte de que la instancia de I+D no active la detección de cambios, establece el changeDetectionStrategy de EmployeeListComponent en OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector
: 'app-employee-list',
  template
: `...`,
  changeDetection
: ChangeDetectionStrategy.OnPush,
  styleUrls
: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Ahora, cuando el usuario escribe una entrada de texto, la detección de cambios solo se activa para el departamento correspondiente:

Detección de cambios en un subárbol de componentes

Puedes encontrar esta optimización aplicada a la aplicación original aquí.

Puedes obtener más información sobre la estrategia de detección de cambios de OnPush en la documentación oficial de Angular.

Para ver el efecto de esta optimización, ingresa un empleado nuevo en la aplicación en StackBlitz.

Usa canalizaciones puras

Aunque la estrategia de detección de cambios para EmployeeListComponent ahora se establece en OnPush, Angular aún vuelve a calcular el valor numérico de todos los empleados de un departamento cuando el usuario escribe la entrada de texto correspondiente.

Para mejorar este comportamiento, puedes aprovechar los canales puros. Los tubos puros y no puros aceptan entradas y muestran resultados que se pueden usar en una plantilla. La diferencia entre ambos es que una tubería pura volverá a calcular su resultado solo si recibe una entrada diferente de su invocación anterior.

Recuerda que la app calcula un valor para mostrar según el valor numérico del empleado y, luego, invoca el método calculate definido en EmployeeListComponent. Si mueves el cálculo a una barra vertical pura, Angular volverá a calcular la expresión de barra vertical solo cuando cambien sus argumentos. El framework determinará si los argumentos de la tubería cambiaron realizando una verificación de referencia. Esto significa que Angular no realizará ningún recálculo, a menos que se actualice el valor numérico de un empleado.

A continuación, se explica cómo mover el cálculo de la empresa a un tubo llamado CalculatePipe:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
 
if (num === 1 || num === 2) {
   
return 1;
 
}
 
return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name
: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform
(val: number) {
   
return fibonacci(val);
 
}
}

El método transform del canal invoca la función fibonacci. Observa que el tubo es puro. Angular considerará que todos los tubos son puros, a menos que especifiques lo contrario.

Por último, actualiza la expresión dentro de la plantilla para EmployeeListComponent:

<mat-chip-list>
 
<md-chip>
    {{ item.num | calculate }}
 
</md-chip>
</mat-chip-list>

Eso es todo. Ahora, cuando el usuario escriba la entrada de texto asociada con cualquier departamento, la app no volverá a calcular el valor numérico de los empleados individuales.

En la siguiente app, puedes ver lo más fluida que es la escritura.

Para ver el efecto de la última optimización, prueba este ejemplo en StackBlitz.

El código con la optimización de canal puro de la aplicación original está disponible aquí.

Conclusión

Cuando se producen ralentizaciones del entorno de ejecución en una app de Angular, haz lo siguiente:

  1. Genera un perfil de la aplicación con Chrome DevTools para ver de dónde provienen las ralentizaciones.
  2. Presenta la estrategia de detección de cambios OnPush para podar los subárboles de un componente.
  3. Mueve los cálculos pesados a canales puros para permitir que el framework almacenen en caché los valores calculados.