Recherche…


Introduction

Le JavaScript, comme tout langage, exige que nous utilisions judicieusement certaines fonctionnalités du langage. La surutilisation de certaines fonctionnalités peut diminuer les performances, tandis que certaines techniques peuvent être utilisées pour améliorer les performances.

Remarques

Rappelez-vous que l'optimisation prématurée est la racine de tout mal. Ecrivez clairement le code correct, puis, si vous rencontrez des problèmes de performances, utilisez un profileur pour rechercher des zones spécifiques à améliorer. Ne perdez pas de temps à optimiser le code sans affecter de manière significative les performances globales.

Mesurer, mesurer, mesurer. Les performances peuvent souvent être contre-intuitives et évoluer avec le temps. Ce qui est plus rapide maintenant pourrait ne plus l'être dans le futur et dépendre de votre cas d'utilisation. Assurez-vous que toutes les optimisations que vous effectuez s'améliorent, ne nuisent pas aux performances et que le changement en vaut la peine.

Évitez les tentatives / prises dans des fonctions critiques

Certains moteurs JavaScript (par exemple, la version actuelle de Node.js et les versions plus anciennes de Chrome avant Ignition + TurboFan) n'exécutent pas l'optimiseur sur les fonctions contenant un bloc try / catch.

Si vous avez besoin de gérer des exceptions dans du code à performances critiques, il peut être plus rapide dans certains cas de conserver le composant try / catch dans une fonction distincte. Par exemple, cette fonction ne sera pas optimisée par certaines implémentations:

function myPerformanceCriticalFunction() {
    try {
        // do complex calculations here
    } catch (e) {
        console.log(e);
    }
}

Cependant, vous pouvez refactoriser pour déplacer le code lent dans une fonction distincte (qui peut être optimisée) et l'appeler depuis le bloc try .

// This function can be optimized
function doCalculations() {
    // do complex calculations here
}

// Still not always optimized, but it's not doing much so the performance doesn't matter
function myPerformanceCriticalFunction() {
    try {
        doCalculations();
    } catch (e) {
        console.log(e);
    }
}

Voici un benchmark jsPerf montrant la différence: https://jsperf.com/try-catch-deoptimization . Dans la version actuelle de la plupart des navigateurs, il ne devrait pas y avoir beaucoup de différence, mais dans les versions moins récentes de Chrome et Firefox, ou IE, la version qui appelle une fonction d'assistance dans try / catch sera probablement plus rapide.

Notez que de telles optimisations doivent être effectuées avec soin et avec des preuves réelles basées sur le profilage de votre code. Au fur et à mesure que les moteurs JavaScript s'améliorent, cela pourrait nuire aux performances au lieu de les aider ou de ne faire aucune différence (mais compliquer le code sans raison). Que cela aide, blesse ou ne fasse aucune différence peut dépendre de nombreux facteurs, donc mesurez toujours les effets sur votre code. C'est vrai pour toutes les optimisations, mais surtout pour les micro-optimisations comme celle-ci, qui dépendent des détails de bas niveau du compilateur / runtime.

Utiliser un mémoizer pour les fonctions de calcul intensif

Si vous créez une fonction qui peut être lourde sur le processeur (client ou serveur), vous pouvez envisager un mémoizer qui est un cache des exécutions de fonctions précédentes et de leurs valeurs renvoyées . Cela vous permet de vérifier si les paramètres d'une fonction ont été passés auparavant. Rappelez-vous, les fonctions pures sont celles qui donnent une entrée, renvoient une sortie unique correspondante et ne provoquent pas d'effets secondaires hors de leur portée. Vous ne devez donc pas ajouter de mémoizer à des fonctions imprévisibles ou dépendantes de ressources externes valeurs retournées).

Disons que j'ai une fonction factorielle récursive:

function fact(num) {
  return (num === 0)? 1 : num * fact(num - 1);
}

Si je passe de petites valeurs de 1 à 100 par exemple, il n'y aurait pas de problème, mais une fois que nous commencerons à aller plus loin, nous pourrions faire exploser la pile d'appels ou rendre le processus un peu pénible pour le moteur Javascript dans lequel nous le faisons. en particulier si le moteur ne compte pas avec l’optimisation de l’appel (bien que Douglas Crockford affirme que l’optimisation de l’appel est incluse dans l’ES6 natif).

Nous pourrions coder dur notre propre dictionnaire de 1 à dieu-sait-quel nombre avec leurs factorielles correspondantes mais, je ne suis pas sûr si je le conseille! Créons un mémoizer, allons-nous?

var fact = (function() {
  var cache = {}; // Initialise a memory cache object
  
  // Use and return this function to check if val is cached
  function checkCache(val) {
    if (val in cache) {
      console.log('It was in the cache :D');
      return cache[val]; // return cached
    } else {
      cache[val] = factorial(val); // we cache it
      return cache[val]; // and then return it
    }
    
    /* Other alternatives for checking are:
    || cache.hasOwnProperty(val) or !!cache[val]
    || but wouldn't work if the results of those
    || executions were falsy values.
    */
  }

  // We create and name the actual function to be used
  function factorial(num) {
    return (num === 0)? 1 : num * factorial(num - 1);
  } // End of factorial function

  /* We return the function that checks, not the one
  || that computes because  it happens to be recursive,
  || if it weren't you could avoid creating an extra
  || function in this self-invoking closure function.
  */
  return checkCache; 
}());

Maintenant, nous pouvons commencer à l'utiliser:

Voilà comment c'est fait

Maintenant que je commence à réfléchir à ce que je faisais, si je devais augmenter de 1 au lieu de décroissent de num, je pourrais avoir mis en cache tous les factorielles de 1 à num dans le cache récursive, mais je partirai pour vous.

C'est génial mais que faire si nous avons plusieurs paramètres ? C'est un problème? Pas tout à fait, nous pouvons faire quelques astuces comme l'utilisation de JSON.stringify () sur le tableau d'arguments ou même une liste de valeurs dont dépendra la fonction (pour les approches orientées objet). Ceci est fait pour générer une clé unique avec tous les arguments et dépendances inclus.

Nous pouvons également créer une fonction qui "mémorise" d'autres fonctions, en utilisant le même concept de portée que précédemment (en renvoyant une nouvelle fonction qui utilise l'original et a accès à l'objet de cache):

AVERTISSEMENT: la syntaxe ES6, si vous ne l'aimez pas, remplace ... par rien et utilise le var args = Array.prototype.slice.call(null, arguments); tour; remplacez const et let avec var, et les autres choses que vous connaissez déjà.

function memoize(func) {
  let cache = {};

  // You can opt for not naming the function
  function memoized(...args) {
    const argsKey = JSON.stringify(args);
    
    // The same alternatives apply for this example
    if (argsKey in cache) {
      console.log(argsKey + ' was/were in cache :D');
      return cache[argsKey];
    } else {
      cache[argsKey] = func.apply(null, args); // Cache it
      return cache[argsKey]; // And then return it
    }
  }

  return memoized; // Return the memoized function
}

Notez maintenant que cela fonctionnera pour plusieurs arguments mais ne sera pas très utile dans les méthodes orientées objet, je pense, vous aurez besoin d'un objet supplémentaire pour les dépendances. En outre, func.apply(null, args) peut être remplacé par func(...args) car la déstructuration des tableaux les enverra séparément plutôt que sous forme de tableau. Aussi, juste pour référence, le passage d'un tableau en tant qu'argument à func ne fonctionnera que si vous utilisez Function.prototype.apply comme je l'ai fait.

Pour utiliser la méthode ci-dessus, vous n'avez qu'à:

const newFunction = memoize(oldFunction);

// Assuming new oldFunction just sums/concatenates:
newFunction('meaning of life', 42);
// -> "meaning of life42"

newFunction('meaning of life', 42); // again
// => ["meaning of life",42] was/were in cache :D
// -> "meaning of life42"

Analyse comparative de votre code - mesure du temps d'exécution

La plupart des astuces de performance sont très dépendantes de l'état actuel des moteurs JS et ne devraient être pertinentes qu'à un moment donné. La loi fondamentale de l'optimisation des performances est que vous devez d'abord mesurer avant d'essayer d'optimiser et mesurer à nouveau après une optimisation présumée.

Pour mesurer le temps d'exécution du code, vous pouvez utiliser différents outils de mesure du temps, tels que:

Interface de performance qui représente les informations de performance liées à la synchronisation pour la page donnée (disponible uniquement dans les navigateurs).

process.hrtime sur Node.js vous donne des informations de synchronisation sous la forme de tuples [secondes, nanosecondes]. Appelé sans argument, il renvoie un temps arbitraire, mais appelé avec une valeur renvoyée précédemment en tant qu'argument, il renvoie la différence entre les deux exécutions.

Minuteries de la console console.time("labelName") lance une minuterie que vous pouvez utiliser pour suivre la durée d'une opération. Vous attribuez un nom d'étiquette unique à chaque temporisateur et vous pouvez avoir jusqu'à 10 000 temporisations sur une page donnée. Lorsque vous appelez console.timeEnd("labelName") avec le même nom, le navigateur termine le minuteur pour le nom donné et affiche le temps en millisecondes écoulé depuis le démarrage du minuteur. Les chaînes passées à time () et timeEnd () doivent correspondre, sinon le minuteur ne se terminera pas.

Date.now function Date.now() renvoie l' horodatage actuel en millisecondes, qui correspond à une représentation numérique du temps depuis le 1er janvier 1970 à 00:00:00 UTC jusqu'à maintenant. La méthode now () est une méthode statique de Date, donc vous l'utilisez toujours comme Date.now ().

Exemple 1 utilisant: performance.now()

Dans cet exemple, nous allons calculer le temps écoulé pour l'exécution de notre fonction, et nous allons utiliser la méthode Performance.now () qui retourne un DOMHighResTimeStamp , mesuré en millisecondes, précis au millième de milliseconde.

let startTime, endTime;

function myFunction() {
    //Slow code you want to measure
}

//Get the start time
startTime = performance.now();

//Call the time-consuming function
myFunction();

//Get the end time
endTime = performance.now();

//The difference is how many milliseconds it took to call myFunction()
console.debug('Elapsed time:', (endTime - startTime));

Le résultat dans la console ressemblera à ceci:

Elapsed time: 0.10000000009313226

L'utilisation de performance.now() a la plus grande précision dans les navigateurs avec une précision au millième de milliseconde, mais la compatibilité la plus faible.

Exemple 2 utilisant: Date.now()

Dans cet exemple, nous allons calculer le temps écoulé pour l'initialisation d'un grand tableau (1 million de valeurs), et nous allons utiliser la méthode Date.now()

let t0 = Date.now(); //stores current Timestamp in milliseconds since 1 January 1970 00:00:00 UTC
let arr = []; //store empty array
for (let i = 0; i < 1000000; i++) { //1 million iterations
   arr.push(i); //push current i value
}
console.log(Date.now() - t0); //print elapsed time between stored t0 and now

Exemple 3 utilisant: console.time("label") & console.timeEnd("label")

Dans cet exemple, nous faisons la même tâche que dans l'exemple 2, mais nous allons utiliser les console.time("label") & console.timeEnd("label")

console.time("t"); //start new timer for label name: "t"
let arr = []; //store empty array
for(let i = 0; i < 1000000; i++) { //1 million iterations
   arr.push(i); //push current i value
}
console.timeEnd("t"); //stop the timer for label name: "t" and print elapsed time

Exemple 4 en utilisant process.hrtime()

Dans les programmes Node.js, c'est le moyen le plus précis de mesurer le temps passé.

let start = process.hrtime();

// long execution here, maybe asynchronous

let diff = process.hrtime(start);
// returns for example [ 1, 2325 ]
console.log(`Operation took ${diff[0] * 1e9 + diff[1]} nanoseconds`);
// logs: Operation took 1000002325 nanoseconds

Préférer les variables locales aux globales, aux attributs et aux valeurs indexées

Les moteurs Javascript recherchent d’abord les variables dans la portée locale avant d’étendre leur recherche à de plus grandes étendues. Si la variable est une valeur indexée dans un tableau ou un attribut dans un tableau associatif, elle recherchera d'abord le tableau parent avant de trouver le contenu.

Cela a des implications lorsque vous travaillez avec du code critique de performance. Prenons , par exemple , une commune for la boucle:

var global_variable = 0;
function foo(){
    global_variable = 0;
    for (var i=0; i<items.length; i++) {
        global_variable += items[i];
    }
}

Pour chaque itération for la boucle, le moteur rechercher des items , rechercher la length attribut dans les éléments, la recherche des items à nouveau, rechercher la valeur à l' index i des items , puis enfin rechercher global_variable , essayant d' abord la portée locale avant de vérifier la portée globale.

Une réécriture performante de la fonction ci-dessus est:

function foo(){
    var local_variable = 0;
    for (var i=0, li=items.length; i<li; i++) {
        local_variable += items[i];
    }
    return local_variable;
}

Pour chaque itération dans la réécrite for la boucle, le moteur lookup li , la recherche des items , rechercher la valeur à l' index i et recherche local_variable , cette fois seulement besoin de vérifier la portée locale.

Réutiliser les objets plutôt que de les recréer

Exemple A

var i,a,b,len;
a = {x:0,y:0}
function test(){ // return object created each call
    return {x:0,y:0};
}
function test1(a){ // return object supplied 
    a.x=0;
    a.y=0;
    return a;
}   

for(i = 0; i < 100; i ++){ // Loop A
   b = test();
}

for(i = 0; i < 100; i ++){ // Loop B
   b = test1(a);
}

La boucle B est 4 (400%) fois plus rapide que la boucle A

Il est très inefficace de créer un nouvel objet dans le code de performance. La boucle A appelle la fonction test() qui renvoie un nouvel objet à chaque appel. L'objet créé est ignoré à chaque itération, la boucle B appelle test1() qui nécessite que l'objet soit fourni. Il utilise donc le même objet et évite l’allocation d’un nouvel objet, ainsi que des hits GC excessifs. (GC n'étaient pas inclus dans le test de performance)

Exemple b

var i,a,b,len;
a = {x:0,y:0}
function test2(a){
    return {x : a.x * 10,y : a.x * 10};
}   
function test3(a){
    a.x= a.x * 10;
    a.y= a.y * 10;
    return a;
}  
for(i = 0; i < 100; i++){  // Loop A
    b = test2({x : 10, y : 10});
}
for(i = 0; i < 100; i++){ // Loop B
    a.x = 10;
    a.y = 10;
    b = test3(a);                
}

La boucle B est 5 fois plus rapide que la boucle A (500%)

Limiter les mises à jour DOM

Une erreur courante rencontrée dans JavaScript lors de l'exécution dans un environnement de navigateur est la mise à jour du DOM plus souvent que nécessaire.

Le problème ici est que chaque mise à jour de l’interface DOM oblige le navigateur à restituer l’écran. Si une mise à jour modifie la disposition d'un élément dans la page, la mise en page entière doit être recalculée, ce qui est très lourd en termes de performances, même dans les cas les plus simples. Le processus de redéfinition d'une page est connu sous le nom de redistribution et peut ralentir l'exécution d'un navigateur ou même ne pas répondre.

La conséquence de la mise à jour trop fréquente du document est illustrée par l'exemple suivant d'ajout d'éléments à une liste.

Considérez le document suivant contenant un élément <ul> :

<!DOCTYPE html>
<html>
    <body>
        <ul id="list"></ul>
    </body>
</html>

Nous ajoutons 5000 éléments à la liste en boucle 5000 fois (vous pouvez essayer cela avec un plus grand nombre sur un ordinateur puissant pour augmenter l'effet).

var list = document.getElementById("list");
for(var i = 1; i <= 5000; i++) {             
    list.innerHTML += `<li>item ${i}</li>`;  // update 5000 times
}

Dans ce cas, les performances peuvent être améliorées en regroupant les 5 000 modifications dans une seule mise à jour du DOM.

var list = document.getElementById("list");
var html = "";
for(var i = 1; i <= 5000; i++) {
    html += `<li>item ${i}</li>`;
}
list.innerHTML = html;     // update once

La fonction document.createDocumentFragment() peut être utilisée comme un conteneur léger pour le code HTML créé par la boucle. Cette méthode est légèrement plus rapide que la modification de la propriété innerHTML l'élément conteneur (comme illustré ci-dessous).

var list = document.getElementById("list");
var fragment = document.createDocumentFragment();
for(var i = 1; i <= 5000; i++) {
    li = document.createElement("li");
    li.innerHTML = "item " + i;
    fragment.appendChild(li);
    i++;
}
list.appendChild(fragment);

Initialisation des propriétés d'objet avec null

Tous les compilateurs JIT JavaScript modernes essayant d'optimiser le code en fonction des structures d'objet attendues. Quelques conseils de mdn .

Heureusement, les objets et les propriétés sont souvent "prévisibles" et, dans ce cas, leur structure sous-jacente peut également être prévisible. Les JIT peuvent compter sur cela pour rendre les accès prévisibles plus rapides.

La meilleure façon de rendre les objets prévisibles est de définir une structure entière dans un constructeur. Donc, si vous voulez ajouter des propriétés supplémentaires après la création de l'objet, définissez-les dans un constructeur avec la valeur null . Cela aidera l'optimiseur à prédire le comportement de l'objet pour l'ensemble de son cycle de vie. Cependant, tous les compilateurs ont des optimiseurs différents, et l'augmentation des performances peut être différente, mais dans l'ensemble, il est recommandé de définir toutes les propriétés d'un constructeur, même si leur valeur n'est pas encore connue.

Temps pour certains tests. Dans mon test, je crée un grand tableau de certaines instances de classe avec une boucle for. Dans la boucle, j'affecte la même chaîne à la propriété "x" de tout objet avant l'initialisation du tableau. Si le constructeur initialise la propriété "x" avec la valeur null, le tableau traite toujours mieux, même s'il effectue une instruction supplémentaire.

C'est du code:

function f1() {
    var P = function () {
        this.value = 1
    };
    var big_array = new Array(10000000).fill(1).map((x, index)=> {
        p = new P();
        if (index > 5000000) {
            p.x = "some_string";
        }

        return p;
    });
    big_array.reduce((sum, p)=> sum + p.value, 0);
}

function f2() {
    var P = function () {
        this.value = 1;
        this.x = null;
    };
    var big_array = new Array(10000000).fill(1).map((x, index)=> {
        p = new P();
        if (index > 5000000) {
            p.x = "some_string";
        }

        return p;
    });
    big_array.reduce((sum, p)=> sum + p.value, 0);
}


(function perform(){
    var start = performance.now();
    f1();
    var duration = performance.now() - start;

    console.log('duration of f1  ' + duration);


    start = performance.now();
    f2();
    duration = performance.now() - start;

    console.log('duration of f2 ' + duration);
})()

C'est le résultat pour Chrome et Firefox.

       FireFox     Chrome
 --------------------------
 f1      6,400      11,400
 f2      1,700       9,600   

Comme nous pouvons le voir, les améliorations de performance sont très différentes entre les deux.

Être cohérent dans l'utilisation des nombres

Si le moteur est capable de prédire correctement que vous utilisez un petit type spécifique pour vos valeurs, il sera en mesure d'optimiser le code exécuté.

Dans cet exemple, nous utiliserons cette fonction triviale en sommant les éléments d'un tableau et en générant le temps nécessaire:

// summing properties
var sum = (function(arr){
        var start = process.hrtime();
        var sum = 0;
        for (var i=0; i<arr.length; i++) {
                sum += arr[i];
        }
        var diffSum = process.hrtime(start);
        console.log(`Summing took ${diffSum[0] * 1e9 + diffSum[1]} nanoseconds`);
        return sum;
})(arr);

Faisons un tableau et additionnons les éléments:

var     N = 12345,
        arr = [];
for (var i=0; i<N; i++) arr[i] = Math.random();

Résultat:

Summing took 384416 nanoseconds

Maintenant, faisons la même chose mais avec seulement des entiers:

var     N = 12345,
        arr = [];
for (var i=0; i<N; i++) arr[i] = Math.round(1000*Math.random());

Résultat:

Summing took 180520 nanoseconds

La somme des nombres entiers a pris la moitié du temps ici.

Les moteurs n'utilisent pas les mêmes types que ceux que vous avez en JavaScript. Comme vous le savez probablement, tous les nombres dans JavaScript sont des nombres à virgule flottante à double précision IEEE754, il n'y a pas de représentation spécifique disponible pour les entiers. Mais les moteurs, quand ils peuvent prédire que vous utilisez uniquement des entiers, peuvent utiliser une représentation plus compacte et plus rapide, par exemple des entiers courts.

Ce type d’optimisation est particulièrement important pour les applications de calcul ou à forte intensité de données.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow