Ottimizza le attività lunghe

Ti è stato detto di non bloccare il thread principale e di disporre le attività più lunghe, ma cosa significa fare queste cose?

I consigli più comuni per mantenere veloci le app JavaScript si basano principalmente sui seguenti consigli:

  • "Non bloccare il thread principale."
  • "Suddividi le tue attività lunghe."

Si tratta di un ottimo consiglio, ma come funziona? Spedire meno codice JavaScript va bene, ma ciò equivale automaticamente a utilizzare interfacce utente più reattive? Forse, ma forse no.

Per capire come ottimizzare le attività in JavaScript, devi prima sapere quali sono le attività e in che modo il browser le gestisce.

Che cos'è un'attività?

Per attività si intende qualsiasi operazione riservata svolta dal browser. Questo tipo di lavoro include il rendering, l'analisi del codice HTML e CSS, l'esecuzione di JavaScript e altri tipi di lavori su cui potresti non avere il controllo diretto. Di tutto questo, il codice JavaScript che scrivi è forse la principale fonte di attività.

Una visualizzazione di un'attività come descritta nel profliler delle prestazioni di DevTools di Chrome. L'attività si trova in cima a una pila, con un gestore di eventi clic, una chiamata di funzione e altri elementi 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à associate a JavaScript influiscono sulle prestazioni in due modi:

  • Quando un browser scarica un file JavaScript durante l'avvio, accoda le attività per l'analisi e la compilazione del codice JavaScript in modo che possa essere eseguito in un secondo momento.
  • Altre volte nel corso della vita della pagina, le attività vengono messe in coda quando JavaScript funziona, ad esempio guidando le interazioni tramite gestori di eventi, animazioni basate su JavaScript e attività in background come la raccolta di dati e analisi.

Tutte queste operazioni, ad eccezione dei web worker e di API simili, si verificano sul thread principale.

Qual è il thread principale?

Il thread principale è il luogo in cui viene eseguita la maggior parte delle attività nel browser e dove viene eseguita 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 è un'attività lunga. Per le attività che superano i 50 millisecondi, il tempo totale meno 50 millisecondi dell'attività è noto come periodo di blocco dell'attività.

Il browser blocca le interazioni durante l'esecuzione di un'attività di qualsiasi lunghezza, ma questo non è percepito dall'utente purché le attività non vengano eseguite troppo a lungo. Quando un utente tenta di interagire con una pagina per cui sono previste molte attività lunghe, tuttavia, l'interfaccia utente non risponde e potrebbe anche non funzionare se il thread principale viene bloccato per periodi di tempo molto lunghi.

Un'attività lunga nel profiler delle prestazioni di DevTools di Chrome. La parte che blocca l'attività (superiore a 50 millisecondi) è rappresentata da un motivo di strisce diagonali rosse.
Un'attività lunga come illustrato nel Performance Profiler di Chrome. Le attività lunghe sono indicate da un triangolo rosso nell'angolo dell'attività, con la parte bloccata all'interno di un motivo a strisce rosse diagonali.

Per evitare che il thread principale venga bloccato per troppo tempo, puoi suddividere un'attività lunga in diverse attività più piccole.

Un'unica attività lunga rispetto alla stessa attività suddivisa in attività più brevi. L'attività lunga è un rettangolo grande, mentre l'attività a blocchi è composta da cinque scatole più piccole che hanno collettivamente la stessa larghezza dell'attività lunga.
Visualizzazione di una singola attività lunga rispetto alla stessa attività suddivisa in cinque attività più brevi.

Questo aspetto è importante perché quando le attività vengono suddivise, il browser può rispondere molto prima alle attività prioritarie, comprese le interazioni degli utenti. In seguito, le attività rimanenti vengono eseguite fino al completamento, assicurando che il lavoro inizialmente messo in coda venga completato.

Una rappresentazione di come suddividere un'attività può facilitare l'interazione di un utente. In alto, un'attività lunga impedisce l'esecuzione di un gestore di eventi fino al termine dell'attività. Nella parte inferiore, l'attività divisa in blocchi consente al gestore di eventi di essere eseguito prima di quanto avrebbe altrimenti.
Una visualizzazione di ciò che accade alle interazioni quando le attività sono troppo lunghe e il browser non può rispondere abbastanza rapidamente alle interazioni, rispetto a quando le attività più lunghe sono suddivise in attività più piccole.

Nella parte superiore della figura precedente, un gestore di eventi messo in coda dall'interazione di un utente ha dovuto attendere una singola attività lunga prima di poter iniziare. Ciò ritarda l'interazione. In questo scenario, l'utente potrebbe aver notato un ritardo. In basso, il gestore di eventi può iniziare a essere eseguito più velocemente e l'interazione potrebbe essere stata istantanea.

Ora che sai perché è importante suddividere le attività, puoi imparare a farlo in JavaScript.

Strategie di gestione delle attività

Un consiglio comune nell'architettura software è suddividere il lavoro in funzioni più piccole:

function saveSettings () {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

In questo esempio, è presente una funzione denominata saveSettings() che chiama cinque funzioni per convalidare un modulo, mostrare una rotellina, inviare dati al backend dell'applicazione, aggiornare l'interfaccia utente e inviare dati e analisi.

Concettualmente, saveSettings() ha un'architettura ben progettata. Se devi eseguire il debug di una di queste funzioni, puoi attraversare l'albero dei progetti per capire lo svolgimento di ciascuna funzione. Suddividere il lavoro in questo modo semplifica la navigazione e la gestione dei progetti.

Un potenziale problema, tuttavia, è che JavaScript non esegue ciascuna di queste funzioni come attività separate perché vengono eseguite all'interno della funzione saveSettings(). Ciò significa che tutte e cinque le funzioni verranno eseguite come un'unica attività.

La funzione saveSettings come illustrato nel Performance Profiler di Chrome. Mentre la funzione di primo livello chiama altre cinque funzioni, tutto il lavoro viene svolto in un'unica lunga attività che blocca il thread principale.
Una singola funzione saveSettings() che chiama cinque funzioni. Il lavoro viene eseguito come parte di un'attività monolitica lunga.

Nella migliore delle ipotesi, anche solo una di queste funzioni può contribuire per almeno 50 millisecondi alla durata totale dell'attività. Nel peggiore dei casi, un numero maggiore di queste attività può essere eseguito molto più a lungo, in particolare su dispositivi con risorse limitate.

Rimanda manualmente l'esecuzione del codice

Un metodo usato dagli sviluppatori per suddividere le attività in attività più piccole è setTimeout(). Con questa tecnica, passi la funzione a setTimeout(). In questo modo, l'esecuzione del callback viene posticipata in un'attività separata, anche se specifichi un timeout pari a 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);
}

Questo processo è noto come rendimento e funziona al meglio per una serie di funzioni che devono essere eseguite in sequenza.

Tuttavia, il codice potrebbe non essere sempre organizzato in questo modo. Ad esempio, potresti avere una grande quantità di dati che devono essere elaborati in un loop e l'attività potrebbe richiedere molto tempo se sono presenti molte iterazioni.

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

L'utilizzo di setTimeout() in questo caso è problematico a causa dell'ergonomia degli sviluppatori e l'elaborazione dell'intera gamma di dati potrebbe richiedere molto tempo, anche se ogni singola iterazione viene eseguita rapidamente. Tutto conta e setTimeout() non è lo strumento giusto per il lavoro, almeno non se usato in questo modo.

Utilizza async/await per creare punti di rendimento

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

Come spiegato in precedenza, è possibile utilizzare setTimeout per accedere al thread principale. Per praticità e una migliore leggibilità, tuttavia, puoi chiamare setTimeout da un Promise e passare il suo metodo resolve come callback.

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

Il vantaggio della funzione yieldToMain() è che puoi await in qualsiasi funzione async. Partendo dall'esempio precedente, potresti creare un array di funzioni da eseguire e cedere al thread principale dopo l'esecuzione di ognuna:

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();
  }
}

Il risultato è che l'attività una volta monolitica è ora suddivisa in attività separate.

La stessa funzione saveSettings presente nel profiler delle prestazioni di Chrome, ma solo con il rendimento. Il risultato è che l'attività una volta monolitica è ora suddivisa in cinque attività separate, una per ogni funzione.
Ora la funzione saveSettings() esegue le sue funzioni figlio come attività separate.

Un'API scheduler dedicata

setTimeout è un modo efficace per suddividere le attività, ma può presentare uno svantaggio: quando passi al thread principale rinviando il codice da eseguire in un'attività successiva, l'attività viene aggiunta alla fine della coda.

Se controlli tutto il codice della tua pagina, puoi creare un programma di pianificazione personalizzato con la possibilità di assegnare la priorità alle attività, ma gli script di terze parti non utilizzeranno il programma di pianificazione. In effetti, non è possibile assegnare le priorità al lavoro in questi ambienti. Puoi solo spezzarli o cedere esplicitamente alle interazioni degli utenti.

Supporto dei browser

  • 94
  • 94
  • x

Origine

L'API scheduler offre la funzione postTask() che consente una pianificazione più granulare delle attività ed è un modo per aiutare il browser a dare la priorità al lavoro in modo che le attività a bassa priorità vengano trasferite al thread principale. postTask() utilizza le promesse e accetta una delle tre impostazioni di priority:

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

Prendi come esempio il codice che segue, in cui l'API postTask() viene utilizzata 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'});
};

In questo caso, la priorità delle attività è pianificata in modo tale che le attività prioritarie del browser, come le interazioni degli utenti, possano svolgersi in mezzo secondo le necessità.

La funzione saveSettings come raffigurata nel profiler delle prestazioni di Chrome utilizza postTask. postTask suddivide ogni funzione in esecuzione e assegna la priorità a questa funzione in modo che un'interazione dell'utente possa essere eseguita senza essere bloccata.
Quando viene eseguito saveSettings(), la funzione pianifica le singole funzioni 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 possono essere eseguite più rapidamente, in quanto il lavoro viene sia suddiviso sia suddiviso in modo appropriato in base alla priorità.

Questo è un esempio semplicistico di come è possibile utilizzare postTask(). È possibile creare un'istanza per diversi oggetti TaskController che possono condividere le priorità tra le attività, inclusa la possibilità di modificare le priorità per diverse istanze TaskController in base alle esigenze.

Rendimento integrato con continuazione tramite l'imminente API scheduler.yield()

Una proposta aggiunta all'API scheduler è scheduler.yield(), un'API progettata appositamente per il trasferimento al thread principale nel browser. Il suo utilizzo è simile alla funzione yieldToMain() illustrata in precedenza in questa guida:

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 utilizzare yieldToMain(), utilizza await scheduler.yield().

Tre diagrammi che raffigurano attività senza cedimento, cedimento e con rendimento e continuazione. Senza cedere, ci sono attività lunghe. Con il rendimento, ci sono più attività più brevi, ma che potrebbero essere interrotte da altre attività non correlate. Con il rendimento e la continuazione, ci sono più attività più brevi, ma il loro ordine di esecuzione viene mantenuto.
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 ti arrechi nel mezzo di un insieme di attività, le altre attività pianificate continueranno nello stesso ordine dopo il punto di rendimento. In questo modo, il codice di script di terze parti non interrompe l'ordine di esecuzione del codice.

L'utilizzo di scheduler.postTask() con priority: 'user-blocking' ha anche un'alta probabilità di continuazione a causa dell'alta priorità di user-blocking, pertanto questo approccio potrebbe essere utilizzato come alternativa nel frattempo.

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

Non usare isInputPending()

Supporto dei browser

  • 87
  • 87
  • x
  • x

L'API isInputPending() offre un modo per verificare se un utente ha tentato di interagire con una pagina e cedere 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 di attività. Ciò può comportare miglioramenti delle prestazioni notevoli, come descritto in Intent to Ship, per i siti che altrimenti potrebbero non restituire al thread principale.

Tuttavia, dopo il lancio dell'API, la nostra comprensione del rendimento è aumentata, in particolare con l'introduzione di INP. Non consigliamo più di utilizzare questa API. Piuttosto, consigliamo di generare indipendentemente dal fatto che l'input sia in attesa o meno per una serie di motivi:

  • isInputPending() potrebbe restituire erroneamente false nonostante un utente abbia interagito in alcune circostanze.
  • L'input non è l'unico caso in cui le attività dovrebbero restituire. Le animazioni e altri aggiornamenti regolari dell'interfaccia utente possono essere ugualmente importanti per fornire una pagina web reattiva.
  • Da allora sono state introdotte API di rendimento più complete che rispondono a problemi di rendimento, quali scheduler.postTask() e scheduler.yield().

Conclusione

La gestione delle attività è complessa, ma in questo modo la tua pagina risponde più rapidamente alle interazioni degli utenti. Non esiste un solo consiglio per gestire e assegnare le priorità alle attività, ma usare una serie di tecniche diverse. Ribadiamo che questi sono gli aspetti principali da considerare quando si gestiscono le attività:

  • Passa al thread principale per le attività critiche rivolte agli utenti.
  • Assegna una priorità alle attività con postTask().
  • Valuta la possibilità di sperimentare con scheduler.yield().
  • Infine, svolgi il minor lavoro possibile nelle tue funzioni.

Con uno o più di questi strumenti, dovresti essere in grado di strutturare il lavoro nella tua applicazione in modo da dare la priorità alle esigenze dell'utente, garantendo al contempo che le attività meno critiche vengano comunque svolte. in modo da creare un'esperienza utente migliore, più reattiva e più piacevole da usare.

Un ringraziamento speciale a Philip Walton per la sua valutazione tecnica di questa guida.

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