Ti è stato detto "non bloccare il thread principale" e "suddividere le attività 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à.
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.
Per evitare che il thread principale venga bloccato per troppo tempo, puoi suddividere un'attività lunga in diverse attività più piccole.
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.
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à.
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 rimandata a 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 svolte prima delle attività con priorità più bassa, puoi restituire al thread principale interrompendo brevemente la coda di attività per dare al browser le 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.
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.
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 valorepriority
.'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à.
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 usare yieldToMain()
, utilizza
await scheduler.yield()
.
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 erroneamentefalse
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()
escheduler.yield()
.
Conclusione
La gestione delle attività è impegnativa, 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 vengano comunque svolte le attività meno critiche. 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.