--- title: Intensive JavaScript slug: Tools/Performance/Scenarios/Intensive_JavaScript translation_of: Tools/Performance/Scenarios/Intensive_JavaScript original_slug: Outils/Performance/Scenarios/Intensive_JavaScript ---
Par défaut, le navigateur n'utilise qu'un seul thread pour exécuter tout le JavaScript d'une page et pour effectuer les layout, reflows et garbage collections. Cela signifie que les fonctions JavaScript qui mettent trop de temps à s'exécuter peuvent bloquer le thread, empêchant ainsi à la page de répondre et donnant alors une mauvaise expérience utilisateur.
Il est possible d'utiliser les outils Frame rate et Chronologie pour repérer le code JavaScript qui cause des problèmes de performances, et pour mettre en évidence les fonctions qui demandent une attention particulière.
Dans cet article, nous prendrons un exemple d'un site où le JavaScript cause des problèmes de réactivité, et nous utiliserons deux approches différentes pour résoudre ce problème. La première approche est de fractionner les fonctions trop gourmandes en plusieurs morceaux et d'utiliser requestAnimationFrame
pour planifier l'exécution de chaque morceau. La seconde approche est d'exécuter la fonction en entier dans un thread séparé en utilisant un web worker.
Si vous souhaitez expérimenter par vous même tout en lisant, le site web de démonstration est disponible ici.
Il existe également une version vidéo de cet article :
{{EmbedYouTube("Pcc6jQX6JDI")}}
Le site de démonstration ressemble à ceci :
Il y trois contrôles :
requestAnimationFrame()
, soit en utilisant un thread séparé grâce à un worker.Afin de voir le problème de cette page, il faut laisser le bouton de sélection sur "Use blocking call in main thread" (appel de fonction bloquant dans le thread principal), puis faire un enregistrement. Pour ce faire, il faut réaliser les étapes suivantes :
Le résultat sera différent d'une machine à l'autre, mais globalement il devrait ressembler à ceci :
La partie haute est la vue d'ensemble de la chronologie. Cela donne une vue compressée de la Chronologie, qui affiche quels types d'opérations le navigateur effectue durant l'enregistrement. La partie rose indique que le navigateur effectue principalement des calculs CSS et potentiellement des reflows: il s'agit des animations CSS qui s'exécutent tout au long du profil. Il y a également des gros blocs orange, représentant l'exécution de JavaScript, il y a un bloc par appui de bouton "Do pointless computations!".
La partie basse qui est relation avec le résumé de la frise chronologique, montre le frame rate. Celui-ci est bon pendant la plus grande partie de l'enregistrement, mais s'effondre complètement à chaque appui de bouton.
Il est possible de sélectionner une de ces périodes afin d'obtenir des informations plus précises dans la vue principale de la chronologie :
Ici, lorsque le bouton est pressé, le navigateur a exécuté une fonction JavaScript, ou une série de fonctions qui ont bloqué le thread principal pendant 71.73ms, soit plus de trois fois le budget de temps pour une frame (1000ms/60frames = 16.67ms par frame).
Mais quelle est cette fonction qui prend tant de temps ? En passant à la vue du Flame Chart (Graphique JS), il est possible de le découvrir :
Cela révèle la pile d'appels JavaScript à cet instant de l'exécution. Sur le haut de la pile, on trouve une fonction nommée calculatePrimes()
, le nom du fichier dans laquelle elle est contenue ainsi que le numéro de ligne à laquelle elle se trouve est également affiché. Le code incriminé est le suivant :
const iterations = 50; const multiplier = 1000000000; function calculatePrimes(iterations, multiplier) { var primes = []; for (var i = 0; i < iterations; i++) { var candidate = i * (multiplier * Math.random()); var isPrime = true; for (var c = 2; c <= Math.sqrt(candidate); ++c) { if (candidate % c === 0) { // not prime isPrime = false; break; } } if (isPrime) { primes.push(candidate); } } return primes; } function doPointlessComputationsWithBlocking() { var primes = calculatePrimes(iterations, multiplier); pointlessComputationsButton.disabled = false; console.log(primes); }
Il s'agit tout simplement d'un test (mal optimisé) de nombre primaire réalisé 50 fois, pour des nombres assez grands.
La première manière de régler ce problème de performance consiste à fractionner la fonction en plusieurs parties plus restreintes, et de planifier l'exécution de chacune avec requestAnimationFrame()
.
requestAnimationFrame()
ordonne au navigateur d'effectuer une fonction à chaque frame, juste avant qu'il effectue un repaint. Tant que les chaque fonction est raisonnablement courte, le navigateur devrait être capable de de ne pas dépasser le budget temps idéal.
Il est assez facile de fractionner calculatePrimes()
: Il suffit de calculer la primarité de chaque nombre dans une fonction séparée :
function doPointlessComputationsWithRequestAnimationFrame() { function testCandidate(index) { // finishing condition if (index == iterations) { console.log(primes); pointlessComputationsButton.disabled = false; return; } // test this number var candidate = index * (multiplier * Math.random()); var isPrime = true; for (var c = 2; c <= Math.sqrt(candidate); ++c) { if (candidate % c === 0) { // not prime isPrime = false; break; } } if (isPrime) { primes.push(candidate); } // schedule the next var testFunction = testCandidate.bind(this, index + 1); window.requestAnimationFrame(testFunction); } var primes = []; var testFunction = testCandidate.bind(this, 0); window.requestAnimationFrame(testFunction); }
Il est maintenant temps de tester cette version. Pour cela on répète les étapes précédentes. Cette fois par contre, l'enregistrement devrait ressembler à ceci :
Au lieu d'un seul bloc organe continu, chaque pression de bouton révèle une longue séquence de courts blocs orange. Ces blocs sont tous espacés d'une frame, et chacun représente une des fonctions appelées par requestAnimationFrame()
. Il est à noter qu'il n'y a eu que deux pressions de bouton dans ce profil.
Ces appels de fonction sont en parallèle aux blocs roses des animations CSS, et chaque fonction est assez courte pour que le navigateur puisse l'exécuter sans faire baisser le frame rate.
Utiliser requestAnimationFrame
pour résoudre le problème de réactivité a fonctionné ici. Cependant, il existe quelques problèmes potentiels à cette solution :
Cette solution tente de régler le problème en utilisant un web worker. Les web workers permettent d'exécuter du JavaScript dans un thread séparé. Le thread principal et le thread du worker ne peuvent pas s'appeler directement, mais peuvent cependant communiquer via une API de messagerie asynchrone.
Le code du thread principal doit ressembler à ceci :
const iterations = 50; const multiplier = 1000000000; var worker = new Worker("js/calculate.js"); function doPointlessComputationsInWorker() { function handleWorkerCompletion(message) { if (message.data.command == "done") { pointlessComputationsButton.disabled = false; console.log(message.data.primes); worker.removeEventListener("message", handleWorkerCompletion); } } worker.addEventListener("message", handleWorkerCompletion, false); worker.postMessage({ "multiplier": multiplier, "iterations": iterations }); }
La différence avec le code original est que l'on a besoin de :
Un fichier "calculate.js", est également nécessaire, son code est le suivant :
self.addEventListener("message", go); function go(message) { var iterations = message.data.iterations; var multiplier = message.data.multiplier; primes = calculatePrimes(iterations, multiplier); self.postMessage({ "command":"done", "primes": primes }); } function calculatePrimes(iterations, multiplier) { var primes = []; for (var i = 0; i < iterations; i++) { var candidate = i * (multiplier * Math.random()); var isPrime = true; for (var c = 2; c <= Math.sqrt(candidate); ++c) { if (candidate % c === 0) { // not prime isPrime = false; break; } } if (isPrime) { primes.push(candidate); } } return primes; }
Dans le worker, il est nécessaire d'être à l'écoute d'un message donnant l'ordre de démarrer. Il est également nécessaire d'envoyer un message "done" lorsque les calculs sont finis. La partie du code qui réalise les calculs elle n'a pas changé.
Comment s'en tire cette version ? Pour le savoir, il suffit de sélectionner "Use a worker", et de capturer un nouveau profil. Il devrait ressembler à ceci :
Dans ce profil, le bouton a été pressé trois fois. Comparé à l'original, chaque pression de bouton est visible dans le résumé sous la forme de deux blocs orange très courts :
doPointlessComputationsInWorker()
qui gère l'évènement de clic et lance le workerhandleWorkerCompletion()
qui s'exécute lorsque le worker appelle "donne".Entre ces deux blocs, le worker effectue ses opérations, et n'a aucun effet visible sur le frame rate et donc sur la réactivité du thread principal. Cela peut sembler étrange, mais puisque le worker s'exécute dans thread séparé, l'ordinateur peut profiter de l'architecture multi coeurs, ce qu'un site web à thread unique ne peut pas faire.
La limitation principale des webs workers est que le code qui s'exécute dans un worker n'a pas accès à l'API DOM.