Ottimizza il rilevamento delle modifiche di Angular

Implementa un rilevamento più rapido dei cambiamenti per una migliore esperienza utente.

Angular esegue periodicamente il suo meccanismo di rilevamento delle modifiche in modo che le modifiche al modello dei dati siano riflesse nella vista di un'app. Il rilevamento delle modifiche può essere attivato manualmente o tramite un evento asincrono (ad esempio, un'interazione dell'utente o un completamento XHR).

Il rilevamento delle modifiche è uno strumento potente, ma se viene eseguito molto spesso, può attivare molti calcoli e bloccare il thread principale del browser.

In questo post scoprirai come controllare e ottimizzare il meccanismo di rilevamento dei cambiamenti saltando parti dell'applicazione ed eseguendo il rilevamento delle modifiche solo quando necessario.

Informazioni sul rilevamento delle modifiche di Angular

Per capire come funziona il rilevamento dei cambiamenti di Angular, diamo un'occhiata a un'app di esempio.

Puoi trovare il codice dell'app in questo repository GitHub.

L'app elenca i dipendenti di due reparti di un'azienda, vendite e ricerca e sviluppo, e si compone di due componenti:

  • AppComponent, che è il componente principale dell'app, e
  • Due istanze di EmployeeListComponent, una per le vendite e una per la ricerca e lo sviluppo.

Applicazione di esempio

Puoi visualizzare le due istanze di EmployeeListComponent nel modello per 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>

Per ogni dipendente sono indicati un nome e un valore numerico. L'app trasmette il valore numerico del dipendente a un calcolo aziendale e mostra il risultato sullo schermo.

Ora dai un'occhiata a 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 accetta un elenco di dipendenti e il nome di un dipartimento come input. Quando l'utente prova a rimuovere o aggiungere un dipendente, il componente attiva un output corrispondente. Il componente definisce anche il metodo calculate, che implementa il calcolo aziendale.

Ecco il modello per 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>

Il codice viene ripetuto per tutti i dipendenti dell'elenco e, per ciascuno, viene visualizzato un elemento dell'elenco. Include inoltre un'istruzione ngModel per l'associazione di dati bidirezionale tra l'input e la proprietà label dichiarata in EmployeeListComponent.

Con le due istanze di EmployeeListComponent, l'app forma la seguente struttura ad albero dei componenti:

Struttura dei componenti

AppComponent è il componente principale dell'applicazione. I relativi componenti secondari sono le due istanze di EmployeeListComponent. Ogni istanza include un elenco di elementi (E1, E2 ecc.) che rappresentano i singoli dipendenti del reparto.

Quando l'utente inizia a inserire il nome di un nuovo dipendente nella casella di immissione di un EmployeeListComponent, Angular attiva il rilevamento delle modifiche per l'intero albero dei componenti a partire dal giorno AppComponent. Ciò significa che, mentre l'utente sta digitando il testo di input, Angular ricalcola ripetutamente i valori numerici associati a ciascun dipendente per verificare che non siano cambiati dall'ultimo controllo.

Per verificare quanto possa essere lento, apri la versione non ottimizzata del progetto su StackBlitz e prova a inserire il nome di un dipendente.

Per verificare che il rallentamento provenga dalla funzione fibonacci, configura il progetto di esempio e apri la scheda Prestazioni di Chrome DevTools.

  1. Premi "Control+Maiusc+J" (o "Comando+Opzione+J" su Mac) per aprire DevTools.
  2. Fai clic sulla scheda Rendimento.

per interrompere la registrazione. Una volta che Chrome DevTools ha elaborato tutti i dati di profilazione raccolti, vedrai quanto segue:

Profilazione del rendimento

Se nell'elenco sono presenti molti dipendenti, questo processo potrebbe bloccare il thread dell'interfaccia utente del browser e causare il calo dei frame, causando un'esperienza utente negativa.

Ignorati sottoalbero dei componenti

Quando l'utente digita l'input di testo per il campo vendite EmployeeListComponent, sai che i dati del reparto Ricerca e sviluppo non cambiano, quindi non c'è motivo di eseguire il rilevamento di modifiche sul suo componente. Per assicurarti che l'istanza di ricerca e sviluppo non attivi il rilevamento delle modifiche, imposta changeDetectionStrategy di EmployeeListComponent su OnPush:

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

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

Ora, quando l'utente digita un input di testo, il rilevamento delle modifiche viene attivato solo per il reparto corrispondente:

Rilevamento delle modifiche in un sottoalbero di un componente

Puoi trovare l'ottimizzazione applicata all'applicazione originale qui.

Per saperne di più sulla strategia di rilevamento dei cambiamenti per OnPush, consulta la documentazione ufficiale di Angular.

Per vedere l'effetto di questa ottimizzazione, inserisci un nuovo dipendente nell'applicazione su StackBlitz.

Utilizzo di pipe

Anche se la strategia di rilevamento dei cambiamenti per EmployeeListComponent è ora impostata su OnPush, Angular ricalcola comunque il valore numerico per tutti i dipendenti di un reparto quando l'utente digita il corrispondente input di testo.

Per migliorare questo comportamento, puoi utilizzare i pipei. Sia i tubi puri che quelli impuri accettano input e restituiscono risultati che possono essere utilizzati in un modello. La differenza tra i due è che una barra verticale pura ricalcola il risultato solo se riceve un input diverso dalla chiamata precedente.

Ricorda che l'app calcola un valore da visualizzare in base al valore numerico del dipendente, richiamando il metodo calculate definito in EmployeeListComponent. Se sposti il calcolo in una barra verticale pura, Angular ricalcolerà l'espressione barra verticale solo quando gli argomenti cambiano. Il framework determinerà se gli argomenti della barra verticale sono cambiati eseguendo un controllo del riferimento. Ciò significa che Angular non eseguirà alcun ricalcolo a meno che il valore numerico per un dipendente non venga aggiornato.

Ecco come spostare il calcolo aziendale in una barra verticale denominata 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);
  }
}

Il metodo transform della barra verticale richiama la funzione fibonacci. Nota che la barra verticale è pura. Se non diversamente specificato, Angular considererà tutte le barre verticali invariate.

Infine, aggiorna l'espressione all'interno del modello per EmployeeListComponent:

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

È tutto. Ora, quando l'utente digita l'input di testo associato a un reparto, l'app non ricalcola il valore numerico per i singoli dipendenti.

Nell'app qui sotto puoi vedere quanto è più fluida la digitazione.

Per vedere l'effetto dell'ultima ottimizzazione, prova questo esempio su StackBlitz.

Il codice con l'ottimizzazione pura del pipe dell'applicazione originale è disponibile qui.

Conclusione

Di fronte a rallentamenti del runtime in un'app Angular:

  1. Profila l'applicazione con Chrome DevTools per capire da dove provengono i rallentamenti.
  2. Presenta la strategia di rilevamento delle modifiche di OnPush per eliminare i sottoalbero di un componente.
  3. Sposta i calcoli pesanti in tubi puri per consentire al framework di eseguire la memorizzazione nella cache dei valori calcolati.