Ottimizza il rilevamento delle modifiche di Angular

Implementa un rilevamento delle modifiche più rapido per una migliore esperienza utente.

Angular esegue periodicamente il suo meccanismo di rilevamento delle modifiche per far sì che le modifiche al modello dei dati si riflettano 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 il completamento di un 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 imparerai a controllare e ottimizzare il meccanismo di rilevamento delle modifiche saltando parti della tua applicazione ed eseguendo il rilevamento delle modifiche solo quando necessario.

All'interno del rilevamento delle modifiche di Angular

Per capire come funziona il rilevamento delle modifiche 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 dei due reparti di un'azienda, Vendite e Ricerca e sviluppo, e ha 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 vedere 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 presenti un nome e un valore numerico. L'app passa il valore numerico del dipendente a un calcolo aziendale e visualizza il risultato sullo schermo.

Ora diamo 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 reparto come input. Quando l'utente tenta di rimuovere o aggiungere un dipendente, il componente attiva un output corrispondente. Il componente definisce anche il metodo calculate, che implementa il calcolo dell'attività.

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>

Questo codice esegue l'iterazione su tutti i dipendenti dell'elenco e, per ognuno, visualizza un elemento dell'elenco. Include anche un'istruzione ngModel per l'associazione di dati bidirezionali 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:

Albero dei componenti

AppComponent è il componente principale dell'applicazione. I suoi componenti secondari sono le due istanze di EmployeeListComponent. Ogni istanza ha un elenco di elementi (E1, E2 e così via) 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'intera struttura ad albero dei componenti a partire da AppComponent. Ciò significa che mentre l'utente sta digitando l'input di testo, Angular ricalcola ripetutamente i valori numerici associati a ciascun dipendente per verificare che non siano stati modificati dall'ultimo controllo.

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

Puoi verificare che il rallentamento provenga dalla funzione fibonacci configurando il progetto di esempio e aprendo la scheda Prestazioni di Chrome DevTools.

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

Ora fai clic su Registra (nell'angolo in alto a sinistra del riquadro Rendimento) e inizia a digitare in una delle caselle di testo nell'app. Tra qualche secondo, fai di nuovo clic su Registra per interrompere la registrazione. Una volta che Chrome DevTools avrà elaborato tutti i dati di profilazione raccolti, vedrai qualcosa del genere:

Profilazione del rendimento

Se ci sono molti dipendenti nell'elenco, questo processo potrebbe bloccare il thread dell'interfaccia utente del browser e causare l'interruzione dei frame, con un'esperienza utente negativa.

Ignorare i sottoalberi dei componenti

Quando l'utente digita l'input di testo per il EmployeeListComponent di vendita, sai che i dati del reparto Ricerca e sviluppo non cambiano, quindi non c'è motivo di eseguire il rilevamento delle 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 questa ottimizzazione applicata all'applicazione originale qui.

Per saperne di più sulla strategia di rilevamento delle modifiche di 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 pure

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

Per migliorare questo comportamento, puoi utilizzare le linee guida pure. Sia le pipeline pure che quelle impure accettano input e restituiscono risultati che possono essere utilizzati in un modello. La differenza tra i due è che una pipe pura ricalcola il suo risultato solo se riceve un input diverso dalla sua 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 su una barra verticale pura, Angular ricalcolerà l'espressione barra verticale solo quando i suoi argomenti cambiano. Il framework determinerà se gli argomenti della barra verticale sono cambiati eseguendo un controllo del riferimento. Ciò significa che Angular non eseguirà ricalcoli a meno che non venga aggiornato il valore numerico di un dipendente.

Ecco come spostare il calcolo aziendale in una pipeline 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 il tubo è puro. Angular considera tutte le barre verticali pure, se non diversamente specificato.

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 seguente puoi vedere quanto è più fluida la digitazione.

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

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

Conclusione

In caso di rallentamenti di runtime in un'app Angular:

  1. Profila l'applicazione con Chrome DevTools per vedere la provenienza dei rallentamenti.
  2. Introduci la strategia di rilevamento delle modifiche OnPush per eliminare i sottoalberi di un componente.
  3. Sposta i calcoli intensivi in pipe pure per consentire al framework di eseguire la memorizzazione nella cache dei valori calcolati.