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.
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.
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.
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.
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();
}
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.
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.
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 alcunpriority
.'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.
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()
.
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()
escheduler.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.