Ottimizza le attività lunghe

Alcuni consigli comunemente disponibili per velocizzare le app JavaScript sono: "Non bloccare il thread principale" e "Suddividi le tue attività lunghe". Questa pagina descrive in dettaglio il significato di questi consigli e il motivo per cui l'ottimizzazione delle attività in JavaScript è importante.

Che cos'è un'attività?

Un'attività è qualsiasi attività distinta svolta dal browser. Ciò include il rendering, l'analisi di HTML e CSS, l'esecuzione del codice JavaScript che scrivi e altri aspetti su cui potresti non avere il controllo diretto. Il codice JavaScript delle pagine è una delle principali fonti di attività del browser.

Uno screenshot di un'attività nella pagina dedicata alle prestazioni dei DevTools di Chrome. L'attività si trova nella parte superiore di una pila, con un gestore di eventi di clic, una chiamata di funzione e altri elementi al di sotto. L'attività include anche alcune operazioni di rendering sul lato destro.
Un'attività avviata da un gestore di eventi click, mostrata nel profiler delle prestazioni di Chrome DevTools.

Le attività influiscono sulle prestazioni in diversi modi. Ad esempio, quando un browser scarica un file JavaScript durante l'avvio, mette in coda le attività per analizzare e compilare il codice JavaScript in modo che possa essere eseguito. Più avanti nel ciclo di vita della pagina, altre attività iniziano quando il codice JavaScript funziona, ad esempio generare interazioni tramite gestori di eventi, animazioni basate su JavaScript e attività in background come la raccolta di analisi. Tutto questo, ad eccezione dei web worker e di API simili, si verifica nel thread principale.

Qual è il thread principale?

Il thread principale è il punto in cui viene eseguita la maggior parte delle attività nel browser e dove viene eseguito quasi tutto il codice JavaScript che scrivi.

Il thread principale può elaborare una sola attività alla volta. Qualsiasi attività che richiede più di 50 millisecondi viene conteggiata come attività lunga. Se l'utente cerca di interagire con la pagina durante un'attività lunga o un aggiornamento del rendering, il browser deve attendere di gestire l'interazione, causando latenza.

Un'attività lunga nel profiler delle prestazioni dei DevTools di Chrome. La parte di blocco dell'attività (maggiore di 50 millisecondi) è contrassegnata da strisce diagonali rosse.
Un'attività lunga mostrata nel profiler delle prestazioni di Chrome. Le attività lunghe sono indicate da un triangolo rosso nell'angolo dell'attività, con la parte che blocca l'attività riempita con uno schema di strisce diagonali rosse.

Per evitare questo problema, dividi ogni attività lunga in attività più piccole, ciascuna delle quali richiede meno tempo. In questo caso si parla di suddivisione delle attività lunghe.

Una singola attività lunga e la stessa attività suddivisa in attività più brevi. L'attività lunga è un rettangolo grande e l'attività in blocchi è costituita da cinque riquadri più piccoli la cui lunghezza corrisponde a quella dell'attività lunga.
Una visualizzazione di una singola attività lunga e della stessa attività suddivisa in cinque attività più brevi.

Suddividere le attività offre al browser maggiori opportunità di rispondere a lavori più prioritari, incluse le interazioni degli utenti, tra le altre attività. In questo modo le interazioni avvengono molto più rapidamente, mentre un utente potrebbe aver notato un ritardo mentre il browser aspettava il completamento di un'attività lunga.

La suddivisione di un'attività può facilitare l'interazione degli utenti. In alto, un'attività lunga impedisce l'esecuzione di un gestore di eventi fino al completamento dell'attività. In basso, l'attività in blocchi consente al gestore di eventi di essere eseguito prima del previsto.
Quando le attività sono troppo lunghe, il browser non può rispondere abbastanza rapidamente alle interazioni. Suddividere le attività consente di avere interazioni più rapidamente.

Strategie di gestione delle attività

JavaScript considera ogni funzione come una singola attività, in quanto utilizza un modello di esecuzione fino al completamento. Ciò significa che una funzione che chiama molte altre funzioni, come nell'esempio seguente, deve essere eseguita fino al completamento di tutte le funzioni chiamate, il che rallenta il browser:

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
La funzione saveSettings) mostrata nel profiler delle prestazioni di Chrome. Anche se la funzione di primo livello chiama altre cinque funzioni, tutto il lavoro si svolge in un'unica lunga attività che blocca il thread principale.
Una singola funzione saveSettings() che chiama cinque funzioni. Il lavoro viene svolto nell'ambito di una lunga attività monolitica.

Se il codice contiene funzioni che chiamano più metodi, suddividilo in più funzioni. Ciò non solo offre al browser maggiori opportunità di rispondere all'interazione, ma rende anche il codice più facile da leggere, gestire e scrivere test. Le sezioni seguenti illustrano alcune strategie per suddividere le funzioni lunghe e assegnare la priorità alle attività che le compongono.

Posticipa manualmente l'esecuzione del codice

Puoi posticipare l'esecuzione di alcune attività passando la funzione pertinente a setTimeout(). Funziona anche se specifichi un timeout di 0.

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

Questa opzione è la più indicata per una serie di funzioni che devono essere eseguite in ordine. Il codice organizzato in modo diverso richiede un approccio diverso. L'esempio successivo è una funzione che elabora una grande quantità di dati utilizzando un loop. Più grande è il set di dati, più lungo sarà l'operazione e non c'è necessariamente un buon punto nel ciclo in cui inserire un elemento setTimeout():

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

Fortunatamente, esistono alcune altre API che consentono di posticipare l'esecuzione del codice a un'attività successiva. Ti consigliamo di utilizzare postMessage() per timeout più rapidi.

Puoi anche interrompere il lavoro utilizzando requestIdleCallback(), che però pianifica le attività in base alla priorità più bassa e solo durante il tempo di inattività del browser, il che significa che se il thread principale è particolarmente affollato, le attività pianificate con requestIdleCallback() potrebbero non essere mai eseguite.

Utilizza async/await per creare punti di rendimento

Per assicurarti che le attività importanti rivolte agli utenti vengano eseguite prima delle attività a priorità inferiore, restituisci il thread principale interrompendo brevemente la coda delle attività per dare al browser l'opportunità di eseguire attività più importanti.

Il modo più chiaro per farlo prevede una Promise che si risolve con una chiamata a setTimeout():

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

Nella funzione saveSettings(), puoi cedere al thread principale dopo ogni passaggio se await la funzione yieldToMain() dopo ogni chiamata di funzione. In questo modo, la lunga attività viene suddivisa in più attività:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread:
    await yieldToMain();
  }
}

Punto chiave: non devi arrenderti dopo ogni chiamata di funzione. Ad esempio, se esegui due funzioni che comportano aggiornamenti critici dell'interfaccia utente, probabilmente non vuoi cedere tra loro. Se puoi, lascia che il lavoro venga eseguito prima, quindi valuta la possibilità di cedere tra funzioni che eseguono operazioni in background o meno operazioni fondamentali che l'utente non vede.

La stessa funzione saveSettings) nel profiler delle prestazioni di Chrome, ora con la resa.
    L'attività è ora suddivisa in cinque attività separate, una per ogni funzione.
La funzione saveSettings() ora esegue le funzioni figlio come attività separate.

Un'API scheduler dedicata

Le API menzionate finora possono aiutarti a suddividere le attività, ma hanno uno svantaggio significativo: quando cedi al thread principale rimandando il codice da eseguire in un'attività successiva, quel codice viene aggiunto alla fine della coda delle attività.

Se controlli tutto il codice della pagina, puoi creare un tuo scheduler per dare priorità alle attività. Tuttavia, gli script di terze parti non utilizzeranno lo scheduler, quindi non potrai dare priorità al lavoro in quel caso. Puoi solo suddividerlo o cedere alle interazioni degli utenti.

Supporto dei browser

  • 94
  • 94
  • x

Fonte

L'API scheduler offre la funzione postTask(), che consente una pianificazione delle attività più granulare e può aiutare il browser a dare priorità al lavoro in modo che le attività a bassa priorità cadano nel thread principale. postTask() usa promesse e accetta un'impostazione priority.

Per l'API postTask() sono disponibili tre priorità:

  • 'background' per le attività con priorità più bassa.
  • 'user-visible' per attività con priorità media. Questa è l'impostazione predefinita se non è impostato alcun priority.
  • 'user-blocking' per attività critiche che devono essere eseguite con priorità elevata.

Il codice di esempio seguente utilizza l'API postTask() per eseguire tre attività con la priorità massima possibile e le due attività rimanenti con la priorità più bassa possibile:

function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});

  // Show the spinner at high priority:
  scheduler.postTask(showSpinner, {priority: 'user-blocking'});

  // Update the database in the background:
  scheduler.postTask(saveToDatabase, {priority: 'background'});

  // Update the user interface at high priority:
  scheduler.postTask(updateUI, {priority: 'user-blocking'});

  // Send analytics data in the background:
  scheduler.postTask(sendAnalytics, {priority: 'background'});
};

Qui, la priorità delle attività è pianificata in modo che le attività prioritarie del browser, come le interazioni con gli utenti, possano funzionare a modo suo.

La funzione saveSettings mostrata nel profiler delle prestazioni di Chrome, ma utilizza postTask. postTask suddivide ogni funzione saveSettings eseguito e assegna la priorità in modo che l'interazione di un utente possa essere eseguita senza essere bloccata.
Quando saveSettings() viene eseguito, la funzione pianifica le singole chiamate di funzione utilizzando postTask(). Il lavoro critico rivolto agli utenti è pianificato con priorità elevata, mentre il lavoro di cui l'utente non è a conoscenza è programmato per essere eseguito in background. In questo modo, le interazioni degli utenti vengono eseguite più rapidamente, perché il lavoro è suddiviso e prioritizzato in modo appropriato.

Puoi anche creare un'istanza TaskController di oggetti TaskController diversi che condividono le priorità tra le attività, inclusa la possibilità di modificare le priorità per diverse istanze TaskController in base alle esigenze.

Rendimento integrato con continuazione che utilizza la prossima API scheduler.yield()

Punto chiave: per una spiegazione più dettagliata di scheduler.yield(), leggi informazioni sulla sua prova dell'origine (da quando è concluso) e sul suo testo esplicativo.

Un'aggiunta proposta all'API scheduler è scheduler.yield(), un'API progettata appositamente per generare il thread principale nel browser. Il suo utilizzo è simile alla funzione yieldToMain() dimostrata in precedenza in questa pagina:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread with the scheduler
    // API's own yielding mechanism:
    await scheduler.yield();
  }
}

Questo codice è molto familiare, ma invece di usare yieldToMain(), usa await scheduler.yield().

Tre diagrammi che mostrano le attività senza cedere, con cedimento, cedimento e continuazione. Senza arrendersi, ci sono attività lunghe. Con la resa, ci sono più attività più brevi, ma che possono essere interrotte da altre attività non correlate. Con rendimento e continuazione, viene mantenuto l'ordine di esecuzione delle attività più brevi.
Quando utilizzi scheduler.yield(), l'esecuzione dell'attività riprende da dove era stata interrotta anche dopo il punto di rendimento.

Il vantaggio di scheduler.yield() è la continuazione, il che significa che se il rendimento avviene nel mezzo di un insieme di attività, le altre attività pianificate continuano nello stesso ordine dopo il punto di rendimento. Questo impedisce agli script di terze parti di assumere il controllo dell'ordine di esecuzione del codice.

L'utilizzo di scheduler.postTask() con priority: 'user-blocking' ha anche un'alta probabilità di continuazione a causa dell'elevata priorità user-blocking, quindi puoi usarla in alternativa fino a quando scheduler.yield() non sarà più disponibile.

L'utilizzo di setTimeout() (o scheduler.postTask() con priority: 'user-visible' o nessun priority esplicito) pianifica l'attività in fondo alla coda, consentendo l'esecuzione di altre attività in sospeso prima della continuazione.

Rendimento in base all'input con isInputPending()

Supporto dei browser

  • 87
  • 87
  • x
  • x

L'API isInputPending() consente di verificare se un utente ha tentato di interagire con una pagina e restituisce solo se un input è in attesa.

In questo modo JavaScript può continuare se non ci sono input in attesa, invece di cedere e finire in fondo alla coda delle attività. Ciò può portare a notevoli miglioramenti delle prestazioni, come descritto nella sezione Intent to Ship, per i siti che altrimenti non cedevano al thread principale.

Tuttavia, dal lancio dell'API in questione, la nostra comprensione del rendimento è migliorata, soprattutto dopo l'introduzione di INP. Non è più consigliabile utilizzare questa API, ma è preferibile cedere indipendentemente dal fatto che l'input sia in attesa o meno. Questa modifica ai consigli è per una serie di motivi:

  • L'API potrebbe restituire erroneamente false nei casi in cui un utente ha interagito.
  • L'input non è l'unico caso in cui dovrebbero essere generati delle attività. Le animazioni e altri normali aggiornamenti dell'interfaccia utente possono essere altrettanto importanti per fornire una pagina web adattabile.
  • Da allora sono state introdotte API più complete e performanti come scheduler.postTask() e scheduler.yield() per risolvere i problemi riscontrati.

Conclusione

Gestire le attività è impegnativo, ma aiuta la tua pagina a rispondere più rapidamente alle interazioni degli utenti. Esistono varie tecniche per gestire e assegnare la priorità alle attività, a seconda del caso d'uso. Ricordate che questi sono gli aspetti principali da considerare quando si gestiscono le attività:

  • Consegna al thread principale per le attività critiche rivolte agli utenti.
  • Valuta la possibilità di sperimentare con scheduler.yield().
  • Dai priorità alle attività con postTask().
  • Infine, svolgi il meno possibile le tue funzioni.

Con uno o più di questi strumenti, dovresti essere in grado di strutturare il lavoro nell'applicazione in modo da dare la priorità alle esigenze dell'utente, garantendo al contempo che venga comunque eseguito il lavoro meno critico. Questo migliora l'esperienza utente, rendendola più reattiva e più piacevole da usare.

Un ringraziamento speciale a Philip Walton per la verifica tecnica di questo documento.

Immagine in miniatura tratta da Unsplash, gentilmente concessa da Amirali Mirhashemian.