Utilisateur:Od1n/Optimisation JavaScript

Performances

modifier
  • Profiling Firebug, exemple avec la page Cinéma (oui, les durées sont bien pour UN chargement de page) :
Function Calls Percent Own Time Time Avg Min Max File
addcache121.05 % 71,653 ms112,395 ms112,395 ms112,395 ms112,395 msCommon.js
process512.52 % 42,628 ms52,486 ms10,497 ms1,087 ms21,125 msCommon.js
loader17,44 %25,317 ms28,342 ms28,342 ms28,342 ms28,342 ms(meta) Wikiminiatlas.js
Sizzle216,35 %21,617 ms28,105 ms1,338 ms0,093 ms3,629 msjquery...?283-13 (line 266)
runOnloadHook25,76 %19,599 ms277,454 ms138,727 ms0,003 ms277,451 mswikibi...?283-13 (line 985)
insertAfter2195,67 %19,309 ms27,05 ms0,124 ms0,11 ms1,082 msCommon.js
showTocToggle15,52 %18,803 ms22,831 ms22,831 ms22,831 ms22,831 mswikibi...?283-13 (line 132)
css104,39 %14,949 ms27,719 ms2,772 ms0,019 ms15,16 msjquery...?283-13 (line 351)
onreadystatechange42,51 %8,54 ms9,104 ms2,276 ms0,006 ms9,081 msindex....ascript (line 116)
curCSS252,16 %7,342 ms12,892 ms0,516 ms0,029 ms11,184 msjquery...?283-13 (line 356)
updateTooltipAccessKeys71,63 %5,552 ms6,217 ms0,888 msms6,194 mswikibi...?283-13 (line 254)
trigger541,56 %5,298 ms13,481 ms0,25 msms6,003 msjquery...?283-13 (line 137)
createNavigationBarToggleButton11,45 %4,928 ms8,867 ms8,867 ms8,867 ms8,867 msCommon.js
getElementsByClassName31,44 %4,893 ms4,893 ms1,631 ms1,191 ms2,405 mswikibi...?283-13 (line 466)
BandeauxPortails_ModifyUl11,33 %4,541 ms11,876 ms11,876 ms11,876 ms11,876 msindex....ascript (line 60)
data1141,18 %4,028 ms4,028 ms0,035 ms0,005 ms2,867 msjquery...?283-13 (line 63)
imageGroup11,07 %3,658 ms3,658 ms3,658 ms3,658 ms3,658 msCommon.js
hasClass6601,07 %3,627 ms3,627 ms0,005 ms0,001 ms0,031 msCommon.js
etc.

Optimisation setModifySectionStyle()

modifier

Code actuel (indentation passée de 8 à 4 espaces, parce que bon...) :

Première proposition de code, basée sur les remarques en dessous

Seconde proposition de code :


Modifications très intéressantes pour les perfs :

  • Le .style.fontWeight = "normal" est complètement inutile
  • Vu que l'ajout du textNode " " a un impact important sur les perfs, voir pour procéder plutôt avec un margin latéral
ok, ça semble jouable un margin-right sur le texte des titres, quelques notes :
  • appliquer directement un style inline, car ajouter une nouvelle classe est plus coûteux et alourdirait le common.css
  • inconvénient : il risque d'y avoir de légères différences de positionnement par rapport à l'existant (un coup c'est 0.25em, un coup c'est 0.3em...), ce qui n'est pas forcément un inconvénient, c'est juste qu'il faudra choisir la valeur à utiliser
  • petit avantage en bonus : quand on sélectionne à la souris le texte du titre, il n'y a plus ce fichu espace à la fin Émoticône sourire
Devoir appliquer un marginRight annule pour une grande partie le gain de perfs dû à la suppression du textNode " ", dommage, mais bon ça reste quand même mieux. Et de toute façon, nous sommes bien obligés d'augmenter l'espacement d'une façon ou une autre.
maquette pour expérimentations : Utilisateur:Od1n/Bac à sable 4 (voir aussi le tableau de valeurs à la fin de cette section)

Autres modifications :

  • Le try/catch semble inutile
à titre de sûreté, peut-être ajouter un « if (!elm.childNode) continue » pour ainsi ne passer lever d'exception si ensuite tentative d'accès au .className d'un élément inexistant (je n'ai pas encore rencontré ce cas, tous les hX ont au moins un childNode, mais cette présomption me semble un peu risquée)
à la réflexion, je pencherais plutôt pour rester sur le try/catch
  • if moins tordu, passe de :
if (!(typeof oldEditsectionLinks == 'undefined' || oldEditsectionLinks == false)) return;
à :
if (typeof oldEditsectionLinks !== 'undefined' && oldEditsectionLinks) return;
  • Au lieu d'invoquer une sous-fonction, on fait :
for (var sections = ["h2", "h3", "h4", "h5", "h6"], i = 0; i < 5; i++) {
    var list = document.getElementsByTagName(sections[i]);
    // suite
}
L'avantage, outre un gain de perf très léger, est que c'est la fonction setModifySectionStyle() qui apparaitra dans les profiling
  • le recours à span.parentNode est inutile vu qu'on connait déjà le parent, c'est list[j]
(mais comme list est un NodeList, penser à bien mettre en cache les accès)

Ajout de fonctionnalité :

  • Modularisation permettant l'utilisation par des scripts ajax (racine différente de document)

Note avant que j'oublie :

  • Dans la version "old style", il existe déjà un textNode vide, entre l'editsection et le mw-headline. Voir pour s'amuser un peu avec (le supprimer, le réutiliser... Tire la langue)


Valeurs de margin-right sur le mw-headline pour avoir un espacement semblable au système avec le textNode " " :

  Firefox 3.6 Chrome IE 8 Opera
h20.25em0.3em0.25em0.25em
h30.3em0.3em0.3em0.3em
h40.3em0.3em0.3em0.25em
h50.3em0.35em0.3em0.3em
h60.3em0.3em0.3em0.3em

Optimisation addcache()

modifier
  • Utilisateur:Od1n/fonction addcache    pour réf rapide, le nettoyeur : $j("small.cachelinks").remove();
  • Retravail du code de sélection des liens à traiter
    • IE : pas de document.getElementsByClassName natif ; le getElementsByClassName "maison" est un désastre pour les perfs, jQuery est bien plus performant.
      petit bonus : compatibilité avec les skins autres que Vector et Monobook (vu qu'on ne se cantonne pas à #bodyContent)
    • autres navigateurs : code basé sur du document.getElementsByClassName natif, que nous allons toutefois hautement optimiser avec une réécriture complète. remarque : il n'y a plus de vérification tag A et OL, mais cela ne devrait théoriquement pas poser de souci. (conservés grâce à la méthode jQuery)
      rappel : document.getElementsByClassName ne retourne pas un array mais un NodeList
  • et en bonus, quelques corrections de bugs (i qui était en globale implicite, setAttribute "class" qui ne fonctionne pas sous IE <= 7)


à voir :

  • comparatif perfs sélecteurs jQuery (rappel : varie grandement selon les versions de IE)
  • .get() superflu ? petite info, .get(undefined) est un alias vers .toArray()
  • gain mise en cache du .length pour les sections références ? (peu nombreux) → on va le faire, car on cherche à encaisser au mieux les plus gros articles, et ils contiennent souvent deux sections ou plus
  • voir si on gagne un peu en mettant en cache liens[k] → oui. légère perte si un seul accès (forcément), gain observable à partir de 2 accès, et ça s'améliore joliment avec le nombre d'accès
  • important : nommer la function, c'est utile (genre pour pas avoir « (?) » dans le profiler)


petit benchmark rapide :

Firefox 3.6 Opera Chrome IE 8
nb itérations 6002000200030
Code origine 1411194032653453
Code retravaillé 1 91634833663
Code retravaillé 2 90739733562
Code retravaillé 3 7446563

Sous Fx, le goulot d'étranglement se situe au niveau des accès propriétés .length des NodeList. edit : nan, pas seulement...

Le code 2 tourne un peu moins bien sur Opera, mais il tourne mieux sur tous les autres navigateurs, et notamment sur les moins performants. de toute façon il y a désormais la 3e méthode, qui met tout le monde d'accord Émoticône

autre remarque, j'ai l'impression que les perfs du code 1 sous Fx se dégradent au fil du temps (succession de tests, pourtant bien isolés avec un module pattern). gestion mémoire foireuse ? pareil Émoticône


rappel : ne pas oublier de considérer la précision du compteur, environ 15 ms

Code actuel :

addOnloadHook(function () {
    if (wgNamespaceNumber == 0) {
        if ((typeof no_external_cache !== "undefined") && (no_external_cache)) {
            return;
        }
        addcache();
    }

    function addcache() {
        // blah
    }
});

... La syntaxe peut prêter à confusion : le hoisting remonte la déclaration de fonction, donc elle est déclarée même si les tests en amont envoient sur le return. Autrement dit, le code suivant est strictement équivalent :

addOnloadHook(function () {
    function addcache() {
        // blah
    }

    if (wgNamespaceNumber == 0) {
        if ((typeof no_external_cache !== "undefined") && (no_external_cache)) {
            return;
        }
        addcache();
    }
});

trois solutions :

  • soit on reste en déclaration classique, la fonction sera alors toujours définie même si le module est désactivé
  • soit on passe en déclaration par expression (var addcache = function () {...}). l'avantage est que l'on économise la déclaration si le module est désactivé. l'inconvénient est que ce mode de déclaration est légèrement moins performant sous Fx.
  • soit on met directement le corps de la fonction et on ne passe pas par cette déclaration supplémentaire a priori superflue. La fonction addcache étant était enclosée dans l'anonymous du hook, donc elle est était de toute façon inaccessible aux autres scripts.

✔️ done : onloadhook seulement si main namespace. pas d'overhead de function imbriquée lorsque le module est activé (c'est le plus important). lorsque le module est désactivé, la function est déclarée, mais elle n'est pas exécutée, alors bon...

Résultat

modifier

Code final

modifier
Des tests de performance supplémentaires sont à effectuer.
La méthode avec les getElementsByClassName natifs est très performante pour rapatrier les données, mais celles-ci sont plus lourdes à traiter (p... de NodeList !)

Bien que la méthode suggérée pour l'instant soit déjà plus performante que la méthode actuellement en production, on devrait pouvoir faire mieux. Sur une partie de mes tests, j'ai été induit en erreur par les NodeList (très lents à l'accès, vu qu'ils sont réactualisés à chaque fois). J'expérimente diverses solutions à base de Array.prototype.slice.call() (attention, soucis de compatibilité), querySelectorAll() et autres joyeusetés. Voire tout simplement jQuery seul, qui en fait ne tourne pas trop mal.

Le problème est que la solution la plus performante s'avère être différente pour chaque navigateur. Émoticône

Benchmark

modifier

Sur l'article Cinéma (218 liens à traiter) – pour 10 itérations :

Firefox 3.6 Opera Chrome IE 8 IE 7
vieux code 12504110648527664203
nouveau code 652128352310002140

Conclusion : le nouveau code est plus efficace dans toutes les situations. Le benchmark le prouve.

Mise à jour

modifier

Nouveau code

modifier

En utilisant tout simplement jQuery :

Nouveau code (encore)

modifier

Quelques améliorations :

  • Modularisation (la function peut ainsi être invoquée par les scripts ajax)
  • Utilisation hasClass() au lieu de .className === "noarchive" (qui peut le plus peut le moins, et principe de moindre surprise)


Benchmark

modifier

Sur l'article Boite de conserve (5 sections Références, 69 liens à traiter en tout), 20 itérations :

Firefox 3.6[1] Opera Chrome IE 8
code actuellement en prod 576 ~ 1189104916211593
ma 1re proposition de code 356 ~ 432494193625
la nouvelle proposition de code 369 ~ 44211698625
  1. les résultats du code actuellement en prod sont très variables avec Firefox, ça sent la mémoire mal gérée, espérons que Firefox 4 arrangera ça


Le code de la nouvelle version est beaucoup plus simple. Et les performances sont bien supérieures sous Opera et Chrome. Le benchmark le prouve.

Les résultats sont un poil moins bons sous Firefox, mais c'est très faible et cela changera peut-être avec les futures versions de Firefox. En tout cas, rien ne justifie de conserver le long code précédent (récupérant les éléments "à la main" pour essayer de faire plus vite que jQuery).

J'ajoute que je suis épaté par les perfs de jQuery, et tout particulièrement son moteur de sélection Sizzle.

Liens utiles

modifier
// classique :
tableau.push(data);

// plus performant :
tableau[tableau.length] = data;

// et bien sûr :
var i = tableau.length;
tableau[i++] = data;