De nombreux sites dits « web 2,0 » utilisent javascript à outrance. Leur performance tient généralement à la bonne ou la mauvaise utilisation de javascript. Aujourd’hui nous pouvons parler du DOM, et l’accès au DOM est lent : évitez le.
Accès DOM
La première étude vient de Dijitalife. L’auteur a comparé le temps nécessaire à l’exécution d’une fonction vide, et comparé avec différent types d’opération. La lecture complète est recommandée. On s’y attend, manipuler un élément prend forcément plus de temps qu’exécuter une fonction vide. Certains temps sont toutefois surprenants.
En comparant la lecture d’un attribut sur un objet javascript et la lecture de l’attribut « id » sur un élément DOM via getAttribute()
, on repère facilement le problème. On est six fois plus lent sur Internet Explorer 7, quatorze fois plus lent sur Firefox 3, et deux fois plus lent sur Safari 3. Quatorze ? le chiffre est énorme pour une information aussi simple que l’identifiant d’un élément déjà récupéré. Même en comparant par rapport à la lecture d’un attribut d’objet javascript via un getter, on reste sept fois plus lent sous Firefox 3 (alors que sous Safari, lire dans le DOM ou dans un objet js via un getter est aussi rapide). Je vous invite à lire le tableau de résultats chiffré sur Dijitalife.
Alors voilà : l’accès au DOM est lent, un peu sous Internet Explorer, beaucoup sous Firefox.
Étonnamment, sur ce même Firefox 3.0 qui met longtemps à lire un attribut DOM, récupérer tous les éléments d’un certain type ou l’élément d’un certain identifiant est rapide (2,5 fois plus que la lecture de l’identifiant dans l’objet DOM ainsi récupéré), sous Internet Explorer 7 elle l’est beaucoup moins (3 à 5 fois plus lent que la lecture de l’identifiant dans l’objet DOM récupéré). Je soupçonne Firefox 3 d’utiliser l’index pour la recherche mais de ne pas importer les différents attributs et valeurs de l’objet avant d’en avoir besoin.
Manipulations de l’arbre DOM
Dans tous les navigateurs, la modification du DOM, que ce soit par innerHTML
ou createTextNode
est lente, mais on s’y attendait. Par contre, changer la valeur className
d’un élément est aussi lent que l’utilisation de innerHTML
sur Internet Explorer 7 ou Safari 3. Pour Firefox 3 c’est toujours lent, mais bien moins qu’un innerHTML
(5 fois plus rapide exactement).
Premières conclusions
Qu’en tirer ? en fait la comparaison entre navigateurs n’apporte pas grand chose par elle-même puisqu’il faut accepter de développer pour tous les navigateurs. De plus, Firefox et Internet Explorer n’ayant pas leur force et leur faiblesse au même endroit pour ce qui est des performances DOM, je serai bien à mal de dire qui a fait le bon choix sans faire des tests (attention toutefois, on parle de performance relative à une opération étalon pour chaque navigateur, même si la performance relative d’Internet Explorer par rapport à son étalon est meilleure, en valeur absolue Firefox reste plus rapide dans tous les cas ou presque).
Par contre il est clair que l’accès au DOM est lent, même en lecture sur de simples attributs. On peut en tirer une recommandation simple : mettez en cache les lectures d’attribut DOM, ne les répétez pas à chaque utilisation.
Jquery a d’ailleurs un Datastore pour éviter ça. Quand vous stockez des informations sur un élément, au lieu d’ajouter des attributs directement dans le DOM (avec toute la lenteur associée), on utilise le datastore
. Conceptuellement c’est juste un attribut unique pour chaque élement DOM, et un dictionnaire qui associe ce que vous voulez stocker avec cet identifiant unique.
Créer ou modifier une structure DOM complexe
La seconde info vient de John Resig. Il nous rappelle la présence des objets DocumentFragment
dans les navigateurs modernes (et même d’autres puisque Internet Explorer 6 supporte cette fonctionnalité). il s’agit d’un espace temporaire, hors du document, où on peut manipuler le DOM, créer des éléments, les supprimer, les déplacer.
John a fait quelques tests, et créer une structure DOM complexe dans un fragment pour la cloner ensuite dans le document réel est bien plus efficace que la créer directement dans le document. On a un gain de performance de 2 à 3. Bien entendu c’est valable si vous souhaitez créer toute une structure, pas un seul noeud.
Sean sullivan en parle aussi, avec des graphiques explicites.
Plus généralement,
- utiliser
cloneNode
est bien plus rapide que recréer à chaque fois des éléments. - créer une structure dans un fragment ou au moins dans un élément détaché de la page jusqu’au dernier moment sera plus rapide que d’ajouter un à un des éléments dans un bloc déjà affichable
- utiliser
innerHTML
sera plus rapide que de créer une structure DOM si vous ne réutilisez pas cette structure ensuite (attention aux fuites de mémoire et aux événements) - modifier des éléments invisibles (
display:none
par exemple) impactera moins les performance (pas de reflow)
Les librairies d’abstraction JS évitent nombre de ces problèmes — comme dans l’exemple de JQuery. Ainsi, je pense qu’utiliser
innerHTML
est une mauvaise pratique. Par ailleurs, je me méfie des conclusions telles que « l’accès est ici x fois plus lent. Il est plus lent relativement à l’autre méthode oui, mais est-il trop lent dans l’absolu, pour l’utilisateur final ?Autant la performance d’un site est à mes yeux essentielle, autant je pense qu’on doit se méfier de l’optimisation aveugle (premature optimization).
Pour les bibliothèques js c’est à double tranchant. Certes choses sont faites pour nous, mais en échange certaines méthodes font parfois plus que nécessaire, ou via une méthode générique qui n’est pas pertinente dans votre cas ou encore (et là c’est parfois dramatique) cache le fonctionnement réel et va faire écrouler les performances.
L’exemple le plus frappant est les méthodes qui sélectionnent des élements à partir d’un sélecteur javascript. Si vous en avez réellement besoin, alors ces méthodes sont de très bonnes abstractions. Si vous n’en avez pas vraiment besoin ou si vous n’utilisez une certaine méthode d’accès alors là c’est parfois dramatique.
Sinon oui, attention à la micro-optimisation, mais certains principes restent bons à connaitre. Stocker quelque chose ? si vous n’avez pas de raison de privilégier une méthode ou une autre, maintenant vous utiliserez une variable javascript et pas un attribut sur un élément du DOM. Construire une structure complexe ? le DomFragment est là pour ça, ça ne coute rien mais il faut connaitre, maintenant c’est fait.
Pour ce qui est de l’optimisation aveugle, vu les performances de la plupart des sites « web 2.0 », c’est à voir. Sur un nombre croissant de sites, ce genre de problématique devient réellement significative. Ce n’est d’ailleurs pas pour rien que Jquery a créé un datastore complexe, c’est bien qu’il y a un problème concret à la base. Même chose, quiconque a tenté de créer un tri javascript sur un tableau peut constater de visu la différence entre modifier le tableau directement ou faire passer un display:none avant.
Savoir après si ce sont des choses pertinentes pour *votre* site c’est une autre histoire, mais personne ne pourra y répondre. En attendant le savoir est toujours intéressant.
Bonjour Eric,
à ta connaissance, YUI a-t-il un système équivalent au datastore de Jquery ?
En même temps, on récupère jamais (rarement) l’identifiant d’un élément DOM, en général, ce que l’on fait c’est l’inverse, on récupère un élément DOM à partir de son id.
Par ailleurs, un des objectifs des bibliothèques javascript est justement de fournir un manière optimisée sur chaque navigateur de faire une opération donnée (par exemple l’ajout de HTML à un endroit). Elles ajoutent une couche d’abstraction qui fait que le codeur n’a pas à chercher à optimiser (entre autre, l’optimisation n’est qu’une problématique) pour chaque type de navigateur.
Si la bibliothèque javascript que vous utilisez ne le fait pas, il faut en changer, ou reporter ceci au mainteneur de la librairie.
Récupérer un identifiant ? non, effectivement, mais c’est un exemple simple. Par contre récupérer des attributs DOM c’est vraiment *très* fréquents. Ca peut être savoir si une case est cochée, la valeur d’un formulaire, les classes d’un élément, le type d’un noeuds, etc. C’est très très fréquent et savoir que c’est lent est utile.
Pour les lib js, la couche d’abstraction ne te permettra jamais d’optimiser. Tout au plus elle permettra d’utiliser la bonne méthode pour faire une opération. Récupérer un attribut sur un élément HTML, surtout un qui n’a pas besoin de calcul, c’est instinctivement une opération basique. Il serait logique que dans une petite boucle on lise cette information trois ou quatre fois (dans un test ou deux, puis au traitement). Aucune raison de passer par une variable temporaire pour un simple accès d’attribut. Sauf que voilà, si tu sais à quel point DOM est lent, tu feras une variable temporaire, et tu auras raison. La lib js ne te sera d’aucun secours ici. Ce n’est pas son rôle d’ailleurs, elle est là pour faire une couche d’abstraction, soit pour simplifier soit pour uniformiser, aucune n’a la prétention d’optimiser un code javascript (et heureusement parce que le bilan sur les performances est toujours négatif sur ces trucs là).
Utiliser une abstraction et affirmer en conséquence qu’on n’a pas à savoir comment ça fonctionne en dessous sur l’implémentation, c’est le meilleur moyen de faire écrouler les performances, quel que soit le système. On le voit dans les ORM quel que soit le langage, dans les libs js avec les query selector, ou à peu près n’importe où. Les utiliser a beaucoup de sens, je ne dis pas de les supprimer, mais n’attendez pas que ça vous permette d’ignorer les questions de performance (au contraire) ou l’implémentation qu’il y a derrière.
@Pascal : pas à ma connaissance pour le datastore, mais en même temps ça répond à une problématique pas si fréquente. Si tu ne stockes qu’un ou deux attributs à toi par objet DOM, je serai très étonné que le datastore jquery accélère quoi que ce soit (ce sera par contre le cas si tu multiplies les données à stocker pour un même élément), simplement parce que pour fonctionner il doit lui même faire des lectures/écritures dans le DOM (c’est juste qu’il te permet de récupérer toutes les data en un seul coup). Maintenant je n’ai qu’à peine regardé le datastore jquery, simplement parce que au jour le jour ce n’est pas jquery que j’utilise, donc je n’exclue pas de bonnes surprises et d’avoir tort (ou au contraire d’avoir tort mais avec une mauvaise surprise).
Bonjour,
Il me parait en effet que l’utilisation d’une librairie JS masque des réalités dont il faut avoir conscience ! Et par exemple l’utilisation aveugle de sélecteurs peut poser problème…
Dans ce sens cet article est bien utile !
Concernant l’optimisation de JQuery, je n’en avais pas entendu parler… Je ne crois pas que les librairies ténor du marché communiquent beaucoup sur ces points ? Dommage…
hello,
Ça me parait un peu expéditif puisque Dom est quand même juste indispensable. Sans compter, que les échanges clients serveurs sont pour la majorité des flux html, xhtml donc XML. De plus, il me semble qu’innerHTML n’appartient pas à DOM. Alors certes, Dom est lent, mais je connais pas mieux sur le marché des apis pour un rendu/ modification/parsage sur nos pages web. Il est recommandation et juste indispensable pour notre web « de surface ».
Pour te punir de ton très vilain article, je te bookmarke et t’obligerais, peut être, dans le futur à subir une fois de plus, mes commentaires 😉
Cordialement ++
Bon article, merci.
Il résume tout à fait ce que je pense : les librairies JS allourdissent parfois le code en masquant ce qui est fait. Les natifs « document.getElementById » ou « document.getElementsByName » sont + rapides que les $ utilisés à outrance (d’ailleurs peu clair au niveau syntaxique).
Le top ? Une fonction comme celle-ci qui détermine le meilleur choix à faire :
function objets(id, classe, parent)
{
if (typeof parent==’undefined’)
parent=document.body;
if (id!=null)
return parent.getElementById;
else
return parent.getElementsByClassName(classe);
}
Cette méthode est évidemment à améliorer pour filtrer les balises, les types… Mais elle quand même + rapide que celles analysant les sélecteurs CSS.