Les conseils les plus courants pour rendre vos applications JavaScript plus rapides sont les suivants : "Ne pas bloquer le thread principal" et "Diviser vos longues tâches". Cette page explique ce que signifie ce conseil et pourquoi il est important d'optimiser les tâches en JavaScript.
Qu'est-ce qu'une tâche ?
Une tâche désigne tout travail discret effectué par le navigateur. Cela inclut l'affichage, l'analyse de code HTML et CSS, l'exécution du code JavaScript que vous écrivez et d'autres éléments sur lesquels vous n'avez peut-être pas de contrôle direct. Le code JavaScript de vos pages est l'une des principales sources de tâches du navigateur.
Les tâches ont un impact sur les performances de plusieurs manières. Par exemple, lorsqu'un navigateur télécharge un fichier JavaScript au démarrage, il met en file d'attente des tâches pour analyser et compiler ce code JavaScript afin qu'il puisse être exécuté. Plus tard dans le cycle de vie de la page, d'autres tâches commencent lorsque votre code JavaScript fonctionne, par exemple la génération d'interactions via des gestionnaires d'événements, des animations basées sur JavaScript et une activité en arrière-plan telle que la collecte d'analyses. Toutes ces opérations se produisent sur le thread principal, à l'exception des nœuds de calcul Web et des API similaires.
Quel est le thread principal ?
Le thread principal est l'endroit où la plupart des tâches s'exécutent dans le navigateur et où presque tout le code JavaScript que vous écrivez est exécuté.
Le thread principal ne peut traiter qu'une tâche à la fois. Toute tâche qui prend plus de 50 millisecondes est considérée comme une tâche longue. Si l'utilisateur tente d'interagir avec la page lors d'une tâche longue ou d'une mise à jour du rendu, le navigateur doit attendre pour gérer cette interaction, ce qui entraîne une latence.
Pour éviter cela, divisez chaque tâche longue en tâches plus petites dont l'exécution prend chacune moins de temps. C'est ce qu'on appelle la décomposition de longues tâches.
La division des tâches donne au navigateur plus de possibilités de répondre entre d'autres tâches aux tâches prioritaires, y compris les interactions utilisateur. Cela permet aux interactions d'avoir lieu beaucoup plus rapidement ; sinon, un utilisateur aurait pu remarquer un décalage pendant que le navigateur attendait l'achèvement d'une longue tâche.
Stratégies de gestion des tâches
JavaScript traite chaque fonction comme une tâche unique, car il utilise un modèle d'exécution de tâche d'exécution jusqu'à la fin. Cela signifie qu'une fonction qui appelle plusieurs autres fonctions, comme dans l'exemple suivant, doit s'exécuter jusqu'à ce que toutes les fonctions appelées soient terminées, ce qui ralentit le navigateur:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Si votre code contient des fonctions qui appellent plusieurs méthodes, divisez-le en plusieurs fonctions. Cela offre non seulement au navigateur davantage de possibilités de répondre à l'interaction, mais cela facilite également la lecture, la gestion et l'écriture de votre code pour les tests. Les sections suivantes présentent certaines stratégies permettant de diviser les fonctions longues et de hiérarchiser les tâches qui les composent.
Reporter manuellement l'exécution du code
Vous pouvez reporter l'exécution de certaines tâches en transmettant la fonction concernée à setTimeout()
. Cela fonctionne même si vous spécifiez un délai avant expiration de 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);
}
Cela fonctionne mieux pour une série de fonctions qui doivent s'exécuter dans l'ordre. Un code organisé différemment nécessite une approche différente. L'exemple suivant est une fonction qui traite une grande quantité de données à l'aide d'une boucle. Plus l'ensemble de données est grand, plus cela prend du temps, et il n'y a pas nécessairement un bon endroit dans la boucle pour placer un setTimeout()
:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Heureusement, il existe quelques autres API qui vous permettent de reporter l'exécution du code dans une tâche ultérieure. Nous vous recommandons d'utiliser postMessage()
pour bénéficier de délais d'inactivité plus rapides.
Vous pouvez également répartir le travail à l'aide de requestIdleCallback()
, mais cela ne planifie les tâches avec la priorité la plus basse que pendant l'inactivité du navigateur. Ainsi, si le thread principal est particulièrement occupé, les tâches planifiées avec requestIdleCallback()
risquent de ne jamais s'exécuter.
Utiliser async
/await
pour créer des points de rendement
Pour vous assurer que les tâches importantes destinées aux utilisateurs sont effectuées avant les tâches de priorité inférieure, abandonnez le thread principal en interrompant brièvement la file d'attente de tâches pour permettre au navigateur d'exécuter des tâches plus importantes.
La méthode la plus claire consiste à utiliser un Promise
qui se résout par un appel à setTimeout()
:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
Dans la fonction saveSettings()
, vous pouvez céder au thread principal après chaque étape si vous utilisez await
la fonction yieldToMain()
après chaque appel de fonction. Cela divise efficacement votre longue tâche en plusieurs tâches:
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();
}
}
Important: Vous n'avez pas besoin de renvoyer après chaque appel de fonction. Par exemple, si vous exécutez deux fonctions qui entraînent des mises à jour critiques de l'interface utilisateur, vous ne souhaitez probablement pas laisser passer entre elles. Si vous le pouvez, laissez ce travail s'exécuter en premier, puis envisagez de céder entre des fonctions qui s'exécutent en arrière-plan ou des tâches moins critiques que l'utilisateur ne voit pas.
Une API de planification dédiée
Les API mentionnées jusqu'à présent peuvent vous aider à répartir les tâches, mais elles présentent un inconvénient important: lorsque vous cédez au thread principal en reportant l'exécution du code dans une tâche ultérieure, ce code est ajouté à la fin de la file d'attente de tâches.
Si vous contrôlez l'ensemble du code de votre page, vous pouvez créer votre propre planificateur pour hiérarchiser les tâches. Toutefois, les scripts tiers n'utilisent pas votre planificateur. Dans ce cas, vous ne pouvez pas vraiment prioriser le travail. Vous pouvez uniquement le diviser ou céder aux interactions de l'utilisateur.
L'API du programmeur offre la fonction postTask()
, qui permet une planification plus précise des tâches et peut aider le navigateur à hiérarchiser les tâches afin que les tâches à faible priorité soient affectées au thread principal. postTask()
utilise des promesses et accepte un paramètre priority
.
L'API postTask()
propose trois priorités:
'background'
pour les tâches dont la priorité est la plus faible.'user-visible'
pour les tâches à priorité moyenne. Il s'agit de la valeur par défaut si aucun élémentpriority
n'est défini.'user-blocking'
pour les tâches critiques devant s'exécuter avec une priorité élevée.
L'exemple de code suivant utilise l'API postTask()
pour exécuter trois tâches avec la priorité la plus élevée possible et les deux tâches restantes à la priorité la plus basse:
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'});
};
Ici, la priorité des tâches est planifiée de sorte que les tâches prioritaires du navigateur, telles que les interactions utilisateur, puissent être prises en compte.
Vous pouvez également instancier différents objets TaskController
qui partagent des priorités entre les tâches, y compris la possibilité de modifier les priorités de différentes instances TaskController
si nécessaire.
Rendement intégré avec continuation à l'aide de la prochaine API scheduler.yield()
Important: Pour obtenir une explication plus détaillée de scheduler.yield()
, consultez son essai d'origine (c'est-à-dire sa phase d'évaluation terminée), ainsi que son explication.
Un ajout proposé à l'API du programmeur est scheduler.yield()
, une API spécialement conçue pour céder au thread principal du navigateur. Son utilisation ressemble à la fonction yieldToMain()
présentée précédemment sur cette page:
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();
}
}
Ce code est très familier, mais au lieu d'utiliser yieldToMain()
, il utilise await scheduler.yield()
.
L'avantage de scheduler.yield()
est la continuation. Cela signifie que si vous générez un rendement au milieu d'un ensemble de tâches, les autres tâches planifiées se poursuivent dans le même ordre après le point de rendement. Cela empêche les scripts tiers de prendre le contrôle de l'ordre dans lequel votre code est exécuté.
L'utilisation de scheduler.postTask()
avec priority: 'user-blocking'
présente également une forte probabilité de continuation en raison de la priorité élevée de user-blocking
. Vous pouvez donc l'utiliser comme alternative jusqu'à ce que scheduler.yield()
devienne plus largement disponible.
L'utilisation de setTimeout()
(ou de scheduler.postTask()
avec priority: 'user-visible'
ou sans priority
explicite) planifie la tâche à la fin de la file d'attente, laissant les autres tâches en attente s'exécuter avant la poursuite.
Cédez en entrée avec isInputPending()
Navigateurs pris en charge
- 87
- 87
- x
- x
L'API isInputPending()
permet de vérifier si un utilisateur a tenté d'interagir avec une page et de ne renvoyer le résultat que si une entrée est en attente.
Cela permet à JavaScript de continuer si aucune entrée n'est en attente, au lieu de céder et de finir à la fin de la file d'attente de tâches. Cela peut entraîner des améliorations de performances impressionnantes, comme détaillé dans l'Intention de livraison, pour les sites qui pourraient autrement ne pas revenir au thread principal.
Toutefois, depuis le lancement de cette API, notre compréhension du rendement s'est améliorée, en particulier depuis l'introduction d'INP. Nous ne recommandons plus d'utiliser cette API. À la place, nous recommandons de générer un résultat, que l'entrée soit en attente ou non. Ce changement dans les recommandations s'explique par plusieurs raisons:
- L'API peut renvoyer de manière incorrecte
false
dans les cas où un utilisateur a interagi. - L'entrée n'est pas le seul cas où les tâches doivent être renvoyées. Les animations et autres mises à jour régulières de l'interface utilisateur peuvent être tout aussi importantes pour fournir une page Web responsive.
- Des API de rendement plus complètes telles que
scheduler.postTask()
etscheduler.yield()
ont depuis été introduites pour résoudre les problèmes de rendement.
Conclusion
La gestion des tâches est un défi, mais cela permet à votre page de répondre plus rapidement aux interactions des utilisateurs. Il existe diverses techniques pour gérer et hiérarchiser les tâches en fonction de votre cas d'utilisation. Pour le rappeler, voici les principaux éléments à prendre en compte lors de la gestion des tâches:
- Basculez vers le thread principal pour les tâches critiques destinées aux utilisateurs.
- Nous vous conseillons de tester
scheduler.yield()
. - Hiérarchisez les tâches avec
postTask()
. - Enfin, effectuez le moins de tâches possible dans vos fonctions.
Avec un ou plusieurs de ces outils, vous devriez être en mesure de structurer le travail dans votre application de manière à donner la priorité aux besoins de l'utilisateur tout en veillant à ce qu'un travail moins important reste effectué. Cela améliore l'expérience utilisateur en la rendant plus réactive et plus agréable à utiliser.
Nous remercions tout particulièrement Philip Walton pour sa vérification technique de ce document.
Vignette Unsplash, fournie par Amirali Mirhashemian.