Come migliorare le prestazioni della tua app Angular

Pubblicato: 2020-04-10

Quando si parla dei migliori framework frontend, è impossibile non menzionare Angular. Tuttavia, richiede molto sforzo da parte dei programmatori per impararlo e usarlo con saggezza. Sfortunatamente, c'è il rischio che gli sviluppatori che non hanno esperienza in Angular possano utilizzare alcune delle sue funzionalità in modo inefficiente.

Una delle tante cose su cui devi sempre lavorare come sviluppatore frontend sono le prestazioni dell'app. Gran parte dei miei progetti passati si concentrava su applicazioni aziendali di grandi dimensioni che continuano ad essere ampliate e sviluppate. I framework frontend sarebbero estremamente utili qui, ma è importante usarli in modo corretto e ragionevole.

Ho preparato un rapido elenco delle strategie e dei suggerimenti più popolari per aumentare le prestazioni che potrebbero aiutarti ad aumentare istantaneamente le prestazioni della tua applicazione Angular . Tieni presente che tutti i suggerimenti qui si applicano ad Angular nella versione 8.

ChangeDetectionStrategy e ChangeDetectorRef

Change Detection (CD) è il meccanismo di Angular per rilevare le modifiche ai dati e reagire automaticamente ad esse. Possiamo elencare il tipo di base delle modifiche allo stato dell'applicazione standard:

  • Eventi
  • Richiesta HTTP
  • Temporizzatori

Queste sono interazioni asincrone. La domanda è: come fa Angular a sapere che si sono verificate alcune interazioni (come clic, intervallo, richiesta http) e che è necessario aggiornare lo stato dell'applicazione?

La risposta è ngZone , che è fondamentalmente un sistema complesso pensato per tenere traccia delle interazioni asincrone. Se tutte le operazioni sono registrate da ngZone, Angular sa quando reagire ad alcune modifiche. Ma non sa cosa è cambiato esattamente e avvia il meccanismo di rilevamento delle modifiche, che controlla tutti i componenti in primo ordine.

Ciascun componente nell'app Angular ha il proprio Rilevatore di modifiche, che definisce come questo componente dovrebbe agire quando è stato avviato il rilevamento delle modifiche, ad esempio se è necessario eseguire nuovamente il rendering del DOM di un componente (che è un'operazione piuttosto costosa). Quando Angular avvia il rilevamento delle modifiche, ogni singolo componente verrà controllato e la sua vista (DOM) potrebbe essere renderizzata nuovamente per impostazione predefinita.

Possiamo evitarlo usando ChangeDetectionStrategy.OnPush:

 @Componente({
  selettore: 'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

Come puoi vedere nel codice di esempio sopra, dobbiamo aggiungere un parametro aggiuntivo al decoratore del componente. Ma come funziona davvero questa nuova strategia di rilevamento delle modifiche?

La strategia dice ad Angular che un componente specifico dipende solo dai suoi @Inputs(). Inoltre, tutti i componenti @Inputs() agiranno come un oggetto immutabile (ad esempio, quando cambiamo solo la proprietà in @Input() di un oggetto, senza modificare il riferimento, questo componente non verrà verificato). Significa che molti controlli non necessari verranno omessi e dovrebbero aumentare le prestazioni della nostra app.

Un componente con ChangeDetectionStrategy.OnPush verrà verificato solo nei seguenti casi:

  • Il riferimento a @Input() cambierà
  • Un evento verrà attivato nel modello del componente o in uno dei suoi figli
  • Osservabile nel componente attiverà un evento
  • Il CD verrà eseguito manualmente utilizzando il servizio ChangeDetectorRef
  • la pipe asincrona viene utilizzata nella vista (la pipe asincrona contrassegna il componente da controllare per le modifiche: quando il flusso di origine emetterà un nuovo valore, questo componente verrà verificato)

Se non si verifica nessuno dei precedenti, l'utilizzo di ChangeDetectionStrategy.OnPush all'interno di un componente specifico fa sì che il componente e tutti i componenti nidificati non vengano controllati dopo l'avvio del CD.

Fortunatamente, possiamo ancora avere il pieno controllo della reazione alle modifiche dei dati utilizzando il servizio ChangeDetectorRef. Dobbiamo ricordare che con ChangeDetectionStrategy.OnPush all'interno dei nostri timeout, richieste, richiamate di abbonamenti, dobbiamo attivare manualmente il CD se abbiamo davvero bisogno di questo:

 contatore = 0;

costruttore(private changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    questo.contatore += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

Come possiamo vedere sopra, chiamando this.changeDetectorRef.detectChanges() all'interno della nostra funzione di timeout , possiamo forzare manualmente il CD . Se il contatore viene utilizzato in qualsiasi modo all'interno del modello, il suo valore verrà aggiornato.

L'ultimo suggerimento in questa sezione riguarda la disabilitazione permanente del CD per componenti specifici. Se abbiamo un componente statico e siamo sicuri che il suo stato non debba essere modificato, possiamo disabilitare il CD in modo permanente:

 this.changeDetectorRef.detach()

Questo codice dovrebbe essere eseguito all'interno del metodo del ciclo di vita ngAfterViewInit() o ngAfterViewChecked(), per essere sicuri che la nostra vista sia stata renderizzata correttamente prima di disabilitare l'aggiornamento dei dati. Questo componente non verrà più controllato durante CD , a meno che non attiviamo detectionChanges() manualmente.

Chiamate di funzione e getter nel modello

L'utilizzo di chiamate di funzione all'interno dei modelli esegue questa funzione ogni volta che il Rilevatore modifiche è in esecuzione. La stessa situazione accade con i getter . Se possibile, dovremmo cercare di evitarlo. Nella maggior parte dei casi, non è necessario eseguire alcuna funzione all'interno del modello del componente durante ogni esecuzione del CD . Invece possiamo usare tubi puri.

Tubi puri

Le pipe pure sono un tipo di pipe con un output che dipende solo dal suo input, senza effetti collaterali. Fortunatamente, tutte le pipe in Angular sono pure per impostazione predefinita.

 @Tubo({
    nome: 'maiuscolo',
    puro: vero
})

Ma perché dovremmo evitare di usare pipe con pure: false? La risposta è di nuovo Rilevamento modifiche. I pipe non puri vengono eseguiti in ogni esecuzione di CD, il che non è necessario nella maggior parte dei casi e riduce le prestazioni della nostra app. Ecco l'esempio della funzione che possiamo cambiare in pure pipe:

 trasforma(valore: stringa, limite = 60, puntini di sospensione = '...') {
  if (!valore || valore.lunghezza <= limite) {
    valore di ritorno;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  return `${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

E vediamo la vista:

 <p class="description">tronca(testo, 30)</p>

Il codice sopra rappresenta la pura funzione: nessun effetto collaterale, l'output dipende solo dagli input. In questo caso, possiamo semplicemente sostituire questa funzione con pure pipe :

 @Tubo({
  nome: 'troncare',
  puro: vero
})
la classe di esportazione TruncatePipe implementa PipeTransform {
  trasforma(valore: stringa, limite = 60, puntini di sospensione = '...') {
    ...
  }
}

E infine, in questa vista, otteniamo il codice, che verrà eseguito solo quando il testo è stato modificato, indipendentemente da Change Detection .

 <p class="descrizione">{{ testo | troncare: 30 }}</p>

Moduli di caricamento lento e precaricamento

Quando la tua applicazione ha più di una pagina, dovresti assolutamente prendere in considerazione la creazione di moduli per ogni parte logica del tuo progetto, in particolare i moduli di caricamento lento . Consideriamo il semplice codice del router Angular:

 cost rotte: Rotte = [
  {
    sentiero: '',
    componente: HomeComponent
  },
  {
    percorso: 'pippo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    percorso: 'bar',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  esportazioni: [RouterModule],
  importazioni: [RouterModule.forRoot(routes)]
})
classe AppRoutingModule {}

Nell'esempio sopra possiamo vedere che il fooModule con tutte le sue risorse verrà caricato solo quando l'utente tenterà di entrare in un percorso specifico (pippo o bar). Angular genererà anche un blocco separato per questo modulo . Il caricamento lento ridurrà il carico iniziale .

Possiamo fare qualche ulteriore ottimizzazione. Supponiamo di voler creare i moduli di caricamento delle nostre app in background. In questo caso, possiamo utilizzare il preloadingStrategy. Angular per impostazione predefinita ha due tipi di preloadingStrategy:

  • Nessun precaricamento
  • Precarica tutti i moduli

Nel codice sopra la strategia NoPreloading viene utilizzata per impostazione predefinita. L'app inizia a caricare un modulo specifico su richiesta dell'utente (quando l'utente vuole vedere un percorso specifico). Possiamo cambiarlo aggiungendo alcune configurazioni extra al router.

 @NgModule({
  esportazioni: [RouterModule],
  importazioni: [RouterModule.forRoot(routes, {
       preloadingStrategy: PreloadAllModules
  }]
})
classe AppRoutingModule {}

Questa configurazione fa sì che il percorso corrente venga mostrato il prima possibile e dopodiché l'applicazione proverà a caricare gli altri moduli in background. Intelligente, non è vero? Ma non è tutto. Se questa soluzione non soddisfa le nostre esigenze, possiamo semplicemente scrivere la nostra strategia personalizzata .

Supponiamo di voler precaricare solo i moduli selezionati, ad esempio BarModule. Lo indichiamo aggiungendo un campo extra per il campo dati.

 cost rotte: Rotte = [
  {
    sentiero: '',
    componente: HomeComponent
    dati: { precarico: falso }
  },
  {
    percorso: 'pippo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    dati: { precarico: falso }
  },
  {
    percorso: 'bar',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    dati: { precarico: vero }
  }
]

Quindi dobbiamo scrivere la nostra funzione di precaricamento personalizzato:

 @Iniettabile()
classe di esportazione CustomPreloadingStrategy implementa PreloadingStrategy {
  preload(route: Route, load: () => Osservabile<qualsiasi>): Osservabile<qualsiasi> {
    ritorno route.data && route.data.preload ? load() : of(null);
  }
}

E impostalo come preloadingStrategy:

 @NgModule({
  esportazioni: [RouterModule],
  importazioni: [RouterModule.forRoot(routes, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
classe AppRoutingModule {}

Per ora verranno precaricate solo le rotte con param { data: { preload: true } }. Il resto dei percorsi agirà come se fosse impostato NoPreloading.

La strategia di precaricamento personalizzata è @Injectable(), quindi significa che possiamo inserire alcuni servizi all'interno se necessario e personalizzare la nostra strategia di precaricamento in qualsiasi altro modo.
Con gli strumenti di sviluppo di un browser, possiamo esaminare l'aumento delle prestazioni uguagliando il tempo di caricamento iniziale con e senza una strategia di precaricamento. Possiamo anche guardare la scheda di rete per vedere che i blocchi per altri percorsi vengono caricati in background, mentre l'utente è in grado di vedere la pagina corrente senza alcun ritardo.

funzione trackBy

Possiamo presumere che la maggior parte delle app Angular utilizzi *ngFor per scorrere gli elementi elencati all'interno del modello. Se l'elenco ripetuto è anche modificabile, trackBy è assolutamente da avere.

 <ul>
  <tr *ngFor="lascia prodotto di prodotti; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId(indice: numero, prodotto: prodotto) {
  restituire product.id;
}

Usando la funzione trackBy, Angular è in grado di tracciare quali elementi delle raccolte sono cambiati (in base all'identificatore dato) e renderizzare nuovamente solo questi elementi particolari. Quando omettiamo trackBy, l'intero elenco verrà ricaricato, il che può essere un'operazione ad alta intensità di risorse su DOM.

Compilazione anticipata (AOT).

Per quanto riguarda la documentazione angolare:

" (...) i componenti e i modelli forniti da Angular non possono essere compresi direttamente dal browser, le applicazioni Angular richiedono un processo di compilazione prima di poter essere eseguite in un browser "

Angular fornisce i due tipi di compilazione:

  • Just-in-Time (JIT): compila un'app nel browser in fase di esecuzione
  • Ahead-of-Time (AOT): compila un'app in fase di compilazione

Per l'utilizzo in fase di sviluppo, la compilazione JIT dovrebbe coprire le esigenze degli sviluppatori. Tuttavia, per la build di produzione dovremmo assolutamente utilizzare AOT . Dobbiamo assicurarci che il flag aot all'interno del file angular.json sia impostato su true. I vantaggi più importanti di tale soluzione includono un rendering più veloce, un minor numero di richieste asincrone, dimensioni di download del framework ridotte e maggiore sicurezza.

Riepilogo

Le prestazioni dell'applicazione sono qualcosa che devi tenere a mente sia durante la parte di sviluppo che di manutenzione del tuo progetto. Tuttavia, la ricerca di possibili soluzioni da soli potrebbe richiedere tempo e fatica. Controllare questi errori comuni e tenerli a mente durante il processo di sviluppo non solo ti aiuterà a migliorare le prestazioni della tua app Angular in pochissimo tempo, ma ti aiuterà anche a evitare errori futuri.

Rilascio dell'icona del prodotto

Affidati a Miquido per il tuo progetto Angular

Contattaci

Vuoi sviluppare un'app con Miquido?

Stai pensando di dare una spinta alla tua attività con un'app Angular? Mettiti in contatto con noi e scegli i nostri servizi di sviluppo di app Angular.