Ottimizza il rilevamento delle modifiche di Angular

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

Angular esegue periodicamente il proprio meccanismo di rilevamento delle modifiche in modo che le modifiche al modello di dati vengano applicate alla visualizzazione 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 una direttiva ngModel per il binding dei 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:

Albero dei componenti

AppComponent è il componente principale dell'applicazione. I 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 digita l'input di testo, Angular ricalcola ripetutamente i valori numerici associati a ciascun dipendente per verificare che non siano cambiati dall'ultimo controllo.

Per capire quanto può essere lento, 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 dell'app. Dopo pochi secondi, fai di nuovo clic su Registra per interrompere la registrazione. Una volta che Chrome DevTools ha elaborato tutti i dati di profilazione raccolti, vedrai qualcosa di simile a quanto segue:

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 di frame, con un'esperienza utente negativa.

Ignorare i sottoalberi dei componenti

Quando l'utente digita l'input di testo per il EmployeeListComponent vendite, sai che i dati del reparto R&D non cambiano, quindi non c'è motivo di eseguire il rilevamento delle modifiche sul relativo componente. Per assicurarti che l'istanza R&D non attivi il rilevamento delle modifiche, imposta il 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 componenti

Puoi trovare questa ottimizzazione applicata all'applicazione originale qui.

Puoi scoprire di più sulla strategia di rilevamento delle modifiche OnPush nella 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 pipe 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 in una barra verticale pura, Angular ricalcolerà l'espressione della barra verticale solo quando cambiano i relativi argomenti. Il framework determinerà se gli argomenti della pipeline 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 dell'attività in una canalizzazione 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 considererà tutte le pipe 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 della pipeline pura dell'applicazione originale è disponibile qui.

Conclusione

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

  1. Esegui il profiling dell'applicazione con Chrome DevTools per capire da dove provengono i 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.