Performance JavaScript, des méthodes simples et efficaces

Dès que l’on part du principe que l’on code pour un navigateur, on se doit d’envisager différents cas de figure. L’utilisateur peut posséder une machine puissante et une excellente connexion, tout comme il peut se trouver sur un ordinateur poussiéreux qui peine déjà à lancer son navigateur dans un vrombissement assourdissant. Pour cette raison, il est nécessaire d’être vigilant et de garder en tête que le script doit être le plus léger, rapide à initialiser et à exécuter possible. Ajoutons à cela un problème supplémentaire : chaque moteur de rendu possède sa manière de traiter le code, ce qui rend l’optimisation plus délicate.

Cet article vous mentionnera quelques techniques simples et efficaces sur la majorité (ou la globalité) des navigateurs.

Maintenabilité du code

Votre source doit être compréhensible et lisible. Lorsque vous travaillez en équipe, définissez des coding-guidelines claires (ASI, laxcomma, indentation, quotes…). C’est le meilleur moyen d’éviter de s’embrouiller dans son code, et d’être ainsi enclin à produire par erreur des soucis de performance.

Les règles à mon sens universelles à ce niveau sont :

  • déclarer les var au début du corps de la fonction ;
  • toujours nommer ses constructeurs avec une première lettre capitale ;
  • ajouter la ligne : if(!(this instanceof myConstructor)) return new myConstructor(args …) au début de myContructor afin d’éviter de polluer window dans les constructeurs en cas d’oubli de new ;
  • utiliser des noms de variables et de méthodes clairs et compréhensibles.

Globales vs locales

Les variables globales étant des propriétés de l’objet window, leur écriture et leur accès sont plus lents. Évitez-les à tout prix et préférez l’utilisation de variables locales. C’est une des raisons pour lesquelles on utilise des self-executing anonymous functions.

(function(){
  var foo = "foo" // foo est une variable locale, unique accessible depuis le corps de cette function. 
})()

Si votre projet est conséquent, divisé en plusieurs scripts ou qu’il vous est nécessaire de conserver des variables accessibles en dehors du corps de la fonction, utilisez un namespace adapté qui ne surcharge pas l’objet window. Par exemple :

(function(){
  var myApp = window.myApp || {}

  myApp.method = function(foo, bar, baz){
    // some code here
  }

   window.myApp = myApp
})()

Ainsi, myApp est accessible depuis des scripts externes.

Petite astuce pour savoir quelles globales sont ajoutées par rapport à l’état normal d’une page : exécutez le code suivant dans la console (le navigateur doit supporter Array#indexOf) ; il vous renverra un array contenant les noms des globales que vous avez ajoutées (volontairement ou non) :

(function(k,l,r,m,i,e,c,o,g){
  for(i in window) k.push(i)
  e = document.createElement("iframe")
  e.style.display = "none"
  document.body.appendChild(e)
  c = e.contentWindow
  for(o in c) l.push(o)
  for(g=k.length;m  g;m++) if(!~l.indexOf(k[m])) r.push(k[m])
  return r
})([],[],[],0)

Minifiez votre HTML en production

Cela peut paraître sans lien, mais réfléchissez‑y : l’indentation et les sauts de ligne de votre source HTML ne sont pas ignorés, et sont présents dans le DOM sous la forme de TextNodes uniquement remplis de whitespace.

<div>
  <h2>Title</h2>
  <p>Text</p>
</div>

représente en réalité dans le DOM :

[HTMLDivElement]
  [TextNode]
  [HTMLTitleElement]
    [TextNode]
  [TextNode]
  [HTMLParagraphElement]
    [TextNode]
  [TextNode]

Minifier votre source HTML permet d’accélérer le parsing du DOM, ainsi que les requêtes que vous y faites.

Les bitwises

Les bitwises sont des opérateurs simples et rapides qui peuvent remplacer des fonctions pour des opérations basiques. Par exemple, un double bitwise not ~ ou un shift à 0 :

~~"14" 
"14" >> 0

permettent de faire strictement la même chose que

parseInt("14", 10)

tout en affichant des performances bien supérieures.

DocumentFragment

Les DocumentFragments (nodeType == 11) sont malheureusement trop peu utilisés. Ils permettent de créer un fragment qui peut contenir des Nodes, hors du DOM. Ce fragment possède les méthodes appendChild et insertBefore et s’avère très utile lorsqu’il s’agit de traiter un certain nombre d’éléments pour les injecter ensuite dans le DOM. On limite ainsi le reflow et l’on peut donner des propriétés aux éléments (ce qu’innerHTML ne peut pas faire sans parser le contenu injecté par le suite). Exemple :

var myFragment = document.createDocumentFragment()
  , i = 0
  , item

function myHandler(){
  var self = this
    , color = self.style.color
  self.style.color = color != "" ? "" : "#c33"
}

for(;i  200; i++) {
  item = document.createElement("li")
  item.innerHTML = i
  item.addEventListener("mouseover", myHandler)
  item.addEventListener("mouseout", myHandler)
  item.className = "item"
  myFragment.appendChild(item)
}

document.getElementById("list").appendChild(myFragment)

Dans le cas de cet exemple, vous ajoutez des éléments li dans #list, en attachant des handlers à certains événements sans impact sur les éléments déjà présents dans #list.

Stockez ce que vous réutilisez

Voici une chose que l’on rencontre souvent dans des scripts utilisant jQuery développés par des personnes qui pensent certainement qu’avec JavaScript, une ligne dont le premier caractère n’est pas $ n’est pas correcte (les créateurs de jQuery incitent pourtant les développeurs à faire disparaître cette mauvaise pratique) :

$("#myId").find(".class").on("foo", function(){
  $(this).css({
    "color" : "#c33"
  })
})

$("#anotherId").on("bar", function(){
  $("#myId").find(".class").hide()
})

Ici, $("#myId").find(".class") est utilisé plusieurs fois. À chaque fois, le DOM est parsé afin de retrouver les éléments. Cela se corrige facilement :

var myElements = $("#myId").find(".class")

myElements.on("foo", function(){
  $(this).css({
    "color" : "#c33"
  })
})

$("#anotherId").on("bar", function(){
  myElements.hide()
})

En plus d’améliorer la performance, on facilite la minification du script.

Optimisez vos boucles

for(var i = 0; i < myArray.length; i++) // action

Ici, myArray.length est réévalué à chaque itération. En effet, for se décompose ainsi :

for(initialize; conditionForCurrentIteration; actionForCurrentIteration)

En outre, la variable i devrait être déclarée plus tôt en même temps que toutes les variables de la fonction en cours afin de prévenir les conflits.

var i = 0
  , length = myArray.length
for(;i  length; i++) // action

Convertissez vos NodeLists en Arrays

Lorsque vous récupérez childNodes ou, par exemple, document.getElementsByTagName("div"), ce n’est pas un Array que vous récupérez mais un [object NodeList]. Les boucles sont extrêmement lentes avec ces derniers (ils sont dynamiques et changent selon les insertions et suppressions du DOM), et ils ne possèdent pas les méthodes d’Array.prototype. Pour remédier à cela :

function toArray(nodeList){
  var i = 0
    , l = nodeList.length
    , result = []
  for(;i  l; i++) result.push(nodeList[i])
  return result
}

toArray(document.getElementsByTagName("div")) // array

Désormais, traiter la liste d’éléments est beaucoup plus rapide.
BONUS — Si vous n’avez pas grand chose à carrer d’Internet Explorer :

Array.prototype.slice.call(nodeList)

fait l’affaire.

Assignez dans if, for et autres

Assigner des variables dans les expressions de type if peut s’avérer extrêmement pratique, notamment si vous n’avez pas besoin d’évaluer l’existence d’un objet avant un certain point de votre fonction :

var el = document.getElementById("foo")
  , parent

if(el && (parent = el.parentNode)) // … action

Utilisez les transitions CSS3

Les animations construites à l’aide de JavaScript sont coûteuses en performance. Les transitions CSS3 sont, quant à elles, gérées par le navigateur lui-même et sont accélérées graphiquement. De plus, définir les transitions CSS3 dans votre fichier .css permet une maintenance bien plus efficace.

L’API classList

classList est une implémentation récente dans Element.prototype. Cela permet de manipuler les classNames d’un élément de manière extrêmement rapide (reflow limité). Lorsqu’elle est supportée, préférez-la à l’utilisation de RegExp. Son utilisation est simpliste :

myElement.classList.add("foo")
myElement.classList.remove("foo")
myElement.classList.toggle("foo")
myElement.classList.contains("foo")

Limitez l’usage des sélecteurs CSS

Les sélecteurs CSS sont agréables à utiliser, simples, etc. Cependant ils sont souvent superflus. Si votre DOM est mal organisé, ils peuvent vous faire gagner du temps et de la performance. Cependant, si vous conservez une hiérarchie correcte, vous n’en avez strictement pas besoin.
Au moins, pour sélectionner par id, vous pouvez utiliser ce petit helper qui gagne jusqu’à 300 % en vitesse.

$.byId = function(id) {
  return $(document.getElementById(id))
}

// USAGE

$.byId("foo").find(".bar")

Sensibilisez-vous à la problématique de la performance

N’utilisez pas systématiquement des frameworks pour effectuer des tâches simples, sensibilisez-vous au souci de performance. Effectuer des benchmarks sur JSPerf est d’une simplicité déconcertante et vous permet d’évaluer des écarts parfois impressionnants entre deux méthodes. C’est d’ailleurs souvent ce genre de détails qui peuvent avoir un impact plutôt inattendu sur la performance globale de votre script.

16 commentaires sur cet article

  1. Jeff, le 8 décembre 2012 à 6:26

    Gagner 2ms de processing ou 2mn a relire son code... hmmm

  2. kzrdt, le 8 décembre 2012 à 9:06

    Article avec rappels intéressants. Enfin n'étant pas un grand utilisateur de Javascript, ce sont même bien plus que des rappels pour moi. Du coup, j'ai cherché dans vos recommandations pour les coding-guidelines ce que signifiaient ASI et laxcomma sans succès... Qu'est-ce?

    Merci

  3. mlb, le 8 décembre 2012 à 11:48

    ASI : Automatic Semicolon Insertion (il s'agit de définir si l'on repose dessus ou non) laxcomma : virgule en début de ligne (pratique en cas d'indentation à 2 espaces) var foo = 1 , bar = 2 , baz = 3

  4. pyrech, le 8 décembre 2012 à 12:41

    Article très intéressant qui rappelle des choses très élémentaires mais que l'on peut vite passer à la trappe si on y fait pas gaffe. Merci ;)

  5. Nicolas Chambrier, le 9 décembre 2012 à 1:34

    Attention, quand on parle de performances il faut des chiffres… Par exemple concernant les double-bitwise versus parseInt, c'est totalement faux: parseInt est à peu près 3 fois plus rapide sur un moteur v8 par exemple. Repéré par @zeroload, et je ne doute pas qu'il y en a d'autres.

    Mes commentaires sur le reste:

    • Concernant les "règles universelles" dont "déclarer les var au début du corps de la fonction". C'est une règle plutôt mauvaise en fait :) avec l'opérateur "let", on aura bien plus de finesse dans les scopes, et déclarer une variable "là où on en a besoin" devrait être, et sera, la règle universelle…

    • "Les variables globales étant des propriétés de l'objet window, leur écriture et leur accès sont plus lents." Rien à voir, on évite les globals tout simplement parce qu'elles peuvent être surchargées par n'importe quelle autre partie du code, ce qui peut donner lieu à des bugs ingérables. Niveau performances, il n'y a strictement aucune différence.

    • "Minifiez votre HTML en production" OK, mais justement vu que les textNodes disparaissent, il y a plutôt intérêt à le minifier aussi en développement, sinon surprise avec childNodes (note qu'il y a "children" pour ça d'ailleurs)…

    • "Optimisez vos boucles" Là encore, need des chiffres. L'accès à une propriété d'un objet est aussi rapide que l'accès à l'objet lui-même (pour peu qu'il n'y ait pas de getters, proxy, ou autre). On risque de gagner vraiment très peu, pour une lisibilité amoindrie. Idem pour la déclaration de la variable (voire plus haut).

    • "Assignez dans if, for et autres" Plutôt une mauvaise pratique, ça rend la relecture bien plus compliquée.

    Sinon j'approuve le reste de l'article (encore que le coup de "boucler sur un NodeList est très lent, alors convertissons-le en tableau… en bouclant dessus" me laisse quand-même dubitatif) ^^

    Merci de m'avoir fait découvrir DocumentFragment d'ailleurs!

  6. mlb, le 9 décembre 2012 à 2:07

    Hi Nicolas,

    Malheureusement, on n'en est pas au let dans le browser …

    Alors si, les globales ont un impact sur la performance : http://mlb.tl/LT61

    Pour la minification, un script qui repose sur le whitespace est bon à être refait (il doit d'ailleurs agir de la même façon avec ou sans whitespace) … et comme tu l'as bien dit, il y a Element#children qui est compatible avec tout ce qu'on peut te demander comme support en prod.

    Les chiffres pour les loops : http://mlb.tl/LTV9 (issus de ce test http://mlb.tl/LSEm)

    Pour les assignements dans if, certes la relecture est compliquée mais ça évite d'assigner trop tôt ou pour rien tout en vérifiant la variable assignée comme je l'ai dit. Cependant je comprends tout à fait que cela soit discutable selon le coding-style de chacun.

    Concernant les NodeLists, boucler une fois au départ permet de d'obtenir un Array qui rentabilise la conversion au bout d'une opération (et tu récupères Array.prototype) (http://mlb.tl/LSjM)

  7. noclat, le 9 décembre 2012 à 2:29

    Super article. Merci beaucoup !

  8. Anthony Ricaud, le 9 décembre 2012 à 9:29

    Je trouve la forme de l'article contre-productive. Oui, il faut faire attention aux performances. Non, il ne faut pas appliquer des techniques généralistes aveuglément. Il faut toujours mesurer ce que l'on fait. Un peu comme le dit le premier commentaire, est-il utile de rendre son code moins lisible si le gain de performance est négligeable ?

    Pour valider les conseils de cet article, il y a de très bon profilers dans les navigateurs. Mesurer vos actions, optimiser les boucles souvent appelées et testez dans plusieurs navigateurs, les optimisations des moteurs JavaScript et DOM variant énormément.

  9. Nicolas Chambrier, le 9 décembre 2012 à 17:22

    @mlb ah ah bien vu pour les perfs des globales, je suis tombé dans le même piège que je te reprochais juste avant, en ne vérifiant pas ;) pan dans les dents :P

    Concernant les loops ton lien montre 1% de différence, pour moi ça confirme plutôt qu'il n'y a pas de différence.

    Bref, en terme de performance, la règle est surtout de n'optimiser que ce qui doit l'être : si la lisibilité ou la maintenabilité doivent être sacrifiés ne serait-ce qu'un tout petit peu, ça doit être pour un énorme gain de perfs. C'est pour ça que les "check-lists" de ce genre ne font jamais l'unanimité ;)

  10. Grégory Houllier, le 9 décembre 2012 à 17:35

    Très bon article globalement, c'est bien de sensibiliser un maximum de developpeurs sur ce sujet! J'ai cependant quelques remarques sur les bitwises et les NodeList.

    ~~ et parseInt ne fonctionne pas pareil notament sur les grands nombre : ~~"3000000000" retourne -1294967296 parseInt("3000000000", 10) retourne bien 3000000000

    Concernant les NodeList, ce n'est pas forcement le fait qu'elles soit live qui pose problème, querySelectorAll retourne des NodeList static mais est beaucoup moins performant que getElementByTagName. http://www.nczonline.net/blog/2010/09/28/why-is-getelementsbytagname-faster-that-queryselectorall/

  11. Geoffrey, le 11 décembre 2012 à 11:08

    Hello,

    Merci pour cet article.

    Je fais partie des utilisateurs de jQuery qui ne savent pas vraiment faire de JS. Mais je me soigne autant que je peux, et ce genre d'article est un bon médoc. :p

    Sinon je vais dans le sens des autres commentaires : j'ai tendance à ne pas sacrifier la lisibilité pour peu de gain de performance (valable en JS, PHP, CSS, etc.), mais chaque cas est à évaluer, bien sûr.

    Bonnes fêtes !

  12. Thomas, le 15 décembre 2012 à 0:50

    Quand vous dites que les animations Js sont moins performantes que les animations CSS3: vous parlez des animations jQuery ou cela s'aplique t'il aussi au Animation Frame Js ?

    Il me semble que ces dernière tirent justement partit du moteur de rendu du navigateur : http://paulirish.com/2011/requestanimationframe-for-smart-animating

    Bien sur, à n'utiliser que lorsqu'on a besoin d'animation plus lourdes qu'un bouton qui tourne :)

  13. Nicolas Chevallier, le 16 décembre 2012 à 19:08

    Merci pour toutes ces bonnes pratiques. Pour la partie minification HTML, avez vous des outils à préconiser? Je cherche en vain une façon sure et simple à mettre en production, comme peut l'être YUI compressor pour CSS/JS par exemple.

  14. CrEv, le 17 décembre 2012 à 10:25

    Je dois dire que je suis plutôt déçu de cet article. J'y ai surtout trouvé pas mal de mauvaises pratiques.

    Si on prend par exemple le code pour voir les globales, il est juste hideux et mal écris alors que peux de temps avant on parle quand même de maintenabilité :

    (function(k,l,r,m,i,e,c,o,g){ for(i in window) k.push(i) e = document.createElement("iframe") e.style.display = "none" document.body.appendChild(e) c = e.contentWindow for(o in c) l.push(o) for(g=k.length;m Par exemple, un double bitwise not ~ ou un shift à 0 : > ~~"14" > "14" >> 0 > permettent de faire strictement la même chose que > parseInt("14", 10) > tout en affichant des performances bien supérieures.

    Oué, sauf que c'est juste faux. Ça ne fait pas strictement la même chose, ça ne le fait que dans des cas limites.

    ~~"14a" == 0 parseInt("14a", 10) == 14 ~~"a14" == 0 parseInt("a14", 10) -> NaN Et donc, isNaN(parseInt("a14", 10)) == true alors que isNaN(~~"a14") == false

    C'est donc pas du tout la même chose, surtout si le but est de traiter des saisies utilisateurs.

    > En outre, la variable i devrait être déclarée plus tôt en même temps que > toutes les variables de la fonction en cours afin de prévenir les conflits. > var i = 0 > , length = myArray.length > for(;i < length; i++) // action

    Pourquoi ? Que la variable soit déclarée en début de code oui. Mais je ne vois pas du tout en quoi l'initialiser en début peut prévenir des conflits, c'est même le contraire car on sépare l'initialisation de son utilisation. var i, length; // bla bla length = myArray.length for (i = 0; i Convertissez vos NodeLists en Arrays

    Il manque quand même une sacré précision. Les boucles sur NodeLists seraient plus lentes que sur Arrays. Soit. Donc la solution est de boucler sur les NodeLists... C'est une technique qui n'est intéressante que si certaines conditions sont respectées :

    • parcourir plus d'une fois la liste des noeuds
    • parcourir tous les noeuds

    Si par exemple on souhaite parcourir une seule fois les noeuds, on perd bien évidemment du temps puisqu'on le fait une première fois pour les placer dans un tableau pour ensuite parcourir le tableau.

    > Assignez dans if, for et autres

    Rien que pour des questions de lisibilité et maintenance on va éviter dans la majorité des cas. A la rigueur sur des while c'est beaucoup plus admis, genre : while((parent = parent.parentNode) !== null)

    Et de manière globale, se méfier (beaucoup) des micro optimisations. Ça a souvent une influence ridicule au réel, voir simplement imperceptible, et ça peut faire perdre énormément en lisibilité et maintenance.

  15. CrEv, le 17 décembre 2012 à 11:54

    Il manque une partie au commentaire, à propos des globales :

    (function(k,l,r,m,i,e,c,o,g){ for(i in window) k.push(i) e = document.createElement("iframe") e.style.display = "none" document.body.appendChild(e) c = e.contentWindow for(o in c) l.push(o) for(g=k.length;m < g;m++) if(!~l.indexOf(k[m])) r.push(k[m]) return r })([],[],[],0)

    Ce code est quand même vraiment mauvais :

    • on a une méthode qui déclare 9 paramètres
    • on en passe 4 avec des valeurs fixes. mais pourquoi ?
    • on en oublie 5. donc pourquoi les avoir ?

    En fait ça utilise les paramètres comme des variables locales, en découplant déclaration (au début) et initialisation (à la fin). C'est juste illisible, le programme n'est pas compréhensible tant qu'on a pas lu la dernière ligne. C'est le boulot des minifieurs / compilateurs de réduire le code, pas au développeur.

    Ça gagnerait beaucoup à être réécrit correctement, surtout peut de temps après un paragraphe sur la "maintenabilité"

    Et je passe sur le for / if / action qui est juste horrible

    (function() { var globalvars = []; var framevars = []; var result = []; var key, frame, content, nbglobalvars, i; for (key in window) { globalvars.push(key); } frame = document.createElement('iframe'); frame.style.display = 'none'; document.body.appendChid(frame); content = frame.contentWindow; for(key in content) { framevars.push(key); } for(i = 0, nbglobalvars = globalvars.length; i < g; i++) { if(!~framevars.indexOf(globalvars[i])) { result.push(globalvars[i]); } } return result; } })();

    Ok, c'est plus long, et on peut le réduire si besoin en virant les accolades même si je ne suis pas fan, surtout lorsqu'on est sur un article expliquant du js. Mais au moins c'est lisible.

    Allez, histoire d'être un peu plus constructif encore :

  16. mlb, le 17 décembre 2012 à 13:38

    Le code pour détecter les globales est fait pour être un bookmarklet. Donc, qu’il soit hideux m’importe peu tant qu’il est court. De plus il n’est pas présenté comme un exemple de maintenabilité mais un code facile à copier-coller dans la console. Concernant ~~, il s’agissait du round de number, l’exemple le signale d’ailleurs. Sachant qu’on déclare la variable plus tôt que l’on connaît la valeur que l’on va lui donner, on peut l’assigner à ce moment. L’assigner dans la boucle ne change rien, je trouve juste ça un peu plus stupide lorsque sa valeur ne dépend pas de code situé entre la déclaration et l’initialisation du for. Il semble évident que la technique de la conversion en Array n’est intéressante qu’à partir de deux loops. Si tu aimes avoir du nesting superflu dans ton code, libre à toi de trouver ça plus lisible; je ne suis pas de cet avis.

    var el = document.getElementById("foo") , parent

    if(el && (parent = el.parentNode)) // … action

    me semble plus clair que

    var el = document.getElementById("foo") , parent

    if(el) { parent = el.parentNode if(parent) { // … action } }

    «se méfier des micro-optimisations». C’est avec ça que tu gagnes en performance. Sur les opérations répétées souvent, un changement qui peut sembler imperceptible ou ridicule ont un impact sur la globalité.

Il n’est plus possible de laisser un commentaire sur les articles mais la discussion continue sur les réseaux sociaux :