Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Les problèmes essentiels de jQuery

Cet article fait suite à la discussion entre Metal3d et Mathieu Robin.

Saluons d'abord l'article de metal3d, qui pose enfin des questions intéressantes après un article écrit trop tôt qui soulignait surtout les difficultés qu'il avait en découvrant jQuery et en essayant de l'utiliser comme Mootools (ce qui ne peut bien sûr pas fonctionner).

Cependant, on sent bien dans cet article que les problèmes fondamentaux commencent à être touchés sans toutefois être exactement saisis. Laissez-moi vous présenter mon point de vue.

La peur des natives

Origine

Il y a un fait qui me frappe en permanence : les personnes qui discutent de jQuery ne relèvent jamais la nature même de la function jQuery() - ou $(). Tout le monde débat de ses conséquences, mais personne ne l'explicite. Permettez donc moi de le faire : jQuery est un énorme decorator.

Quoi que vous vouliez faire avec des éléments DOM et jQuery, il faut d'abord les avoir décoré avec jQuery(). Vous vous êtes probablement tous retrouvés avec des élements que vous vouliez manipulez, pour vous voir dire : "la méthode que vous avez demandée n'existe pas". Et de réaliser aussitôt : "ah, il faut que je le passe à $()". C'est parce que vous aviez un élément qui n'était plus décoré.

L'extrême centralité de ce decorator et le fait que javascript traite les fonctions comme des objets ont fait qu'il est rapidement devenu un namespace. Aujourd'hui, toute la galaxie jQuery se greffe sur $(), soit en tant qu'attribut, soit en tant que prototype.

Mais pourquoi ce choix d'utiliser un decorator comme point unique d'entrée de la library ?

On entendra rabâcher encore et encore qu'il ne faut pas "polluer le scope", ou "polluer les natives", blah. Il s'agit ici de conséquences de cette décision, et non de causes.

La raison principale et historique de ce choix relève des conflits entre libraries. En 2005, lorsque jQuery a été developpé, l'ecosystème javascript était beaucoup plus instable qu'aujourd'hui. Les développeurs, pour la plupart, n'écrivaient pas de javascript. Ceux qui en écrivaient, dans leur vaste majorité, écrivaient en javascript vanilla et chargeaient divers scripts tiers, dédiés chacun à une tâche bien précise. Enfin, il y avait les happy few qui utilisaient le tout jeune Prototype.js .

Au passage, j'entend souvent dire que Prototype et Mootools seraient des ancêtres et que jQuery est le petit nouveau. Voici leurs dates de first release :

Autant dire qu'ils sont tous apparus en même temps. Mais revenons au contexte de l'écriture de jQuery.

Du fait du nombre de scripts specialisés, il y avait souvent des conflits. On ne pouvait pas charger tel scripts qui fait un accordeon et tel script qui fait un menu en drop down, parce que chacun implémentait une méthode walk sur Array, et que les deux méthodes n'étaient bien sûr pas compatibles.

Pire, Prototype.js modifiait quasiment toutes les natives, ainsi. Lorsqu'on voulait poser un script trouvé sur le net sur notre site, il fallait toujours croiser les doigts en espérant qu'il allait être compatible avec Prototype.js (et, si ce n'était pas le cas, qu'on allait s'en rendre compte suffisamment vite).

L'arrivée de jQuery a été un vrai bol d'air, pour ceux qui faisaient déjà du javascript : tout à coup, on avait une lib nous permettant de modifier facilement le DOM sans prise de tête de compatibilité browser, mais sans non plus prise de tête de compatibilité entre les scripts. Parce que ... jQuery avait décidé d'être un énorme decorator ne touchant à aucune native.

jQuery, dès le début, est pensé comme une library spécialisée (dans la manipulation du DOM) faite pour cohabiter avec d'autres libraries. And that, ladies and gents, is why you don't extend natives in jQuery.

Aujourd'hui

Maintenant, on peut raisonnablement poser la question : ce choix a-t-il encore du sens aujourd'hui ? Cherchez sur le net un script tiers pour n'importe quelle fonctionnalité que vous voudriez implémenter à moindre coût. Vous tomberez sur un plugin jQuery.

L'Histoire a rattrapé jQuery : s'il voulait être une lib spécialisée cohabitant avec d'autres libs spécialisées, il est en fait devenu de facto une base de développement, ce qui fait que toutes les autres libs prennent en compte son existance - y compris celles qui veulent pouvoir être utilisées sans jQuery. À vrai dire, ici, nous sommes bien obligés de reconnaître que la vision de Prototype.js l'emporte sur celle de jQuery : aujourd'hui, nous attendons d'une lib javascript qu'elle soit une base de développement multi-purpose solide, et non pas une lib spécialisée naviguant dans un ecosystème d'autres libs.

Ce ne serait pas grave si le choix d'un decorator plutôt que d'extension de natives n'avait pas de conséquences négatives. Il en a.

La première est syntaxique. S'il est possible de chaîner les retours du decorator grâce à la sage règle qu'une fonction jQuery doit toujours retourner this, nous sommes malheureusement bien souvent obligés d'écrire des choses à l'arrière goût impératif. Qui préfère écrire ça :

var human_sum = parseInt( $.each( $.merge( {}, arr1, arr2 ), function( i, val ){ return $.isNumeric( val ) ? val : 0; } ).reduce( function( first, second ){ return first + second; }, 0 ), 10 );

Plutôt que ça ?

var human_sum = arr1.merge( arr2 ).each( function( val ){ return typeOf( val ) == 'number' ? val : 0; }).reduce( function( first, second ){ return first + second; }, 0 ).toInt();

La seconde raison est qu'un objet décoré n'est pas - obviously - cet objet, ce qui est pénible à l'usage. J'ai mentionné plus haut le fait qu'on soit souvent obligé de faire element = $(element). Vous allez me dire que ça relève du détail, mais ceci me rend très triste :

$('#my_element') == $('#my_element') // => false

C'est contournable, encore une fois en rajoutant de la syntaxe qui ne sert qu'à circonvenir au decorator :

$('#my_element').get() == $('#my_element').get() // => true

Bref, ça fait beaucoup de syntaxe hackish pour contourner un problème qui n'existe plus. C'est cela, la pollution.

jQuery pousse au crime

Mais ce point ne relève que d'esthétique. Je ne supporte pas d'écrire du code laid, mais si les autres en sont satisfaits, tant mieux pour eux. Il y a quelque chose de beaucoup plus problématique.

querySelector vs querySelectorAll

Metal3d pointait le problème de query dom qui, avec jQuery, retourne toujours une collection, même si on ne veut qu'un seul élément. C'est un problème fondamental : toute fonction jQuery liée à des élements opère en loopant sur une collection. Si la query ne retourne aucun élément, on loop juste sur une collection vide (l'équivalent d'un [].forEach()), ce qui ne pose aucun problème.

Ça devrait.

Les développeurs ont l'habitude de concaténer leur javascript dans un seul fichier, voir de tout écrire directement dans un même fichier. Typiquement, on se retrouve avec un fichier final comme ceci :

$(function(){
  $( '#sidebar li' ).hover( menuFadeInEffect, menuFadeOutEffect );

  $( '#registration_form' ).submit( validatesForm );

  $( '#admin_user_list a.delete' ).click( userDeletion );

  $( '#books .buy_now' ).click( addToBasket );

  // ... and so on
});

Un jour, ce fichier atteint plusieurs centaines de lignes, nous ouvrons internet explorer, et nous constatons que c'est très lent : "Bah, c'est internet explorer". Non, c'est de notre faute.

Parcourir le DOM est une opération extrêmement coûteuse, qui ne doit être faite que si c'est nécessaire. Dans mon exemple précédent, si je suis sur la home page, la query $( '#admin_user_list a.delete' ) va de toute évidence ne rien retourner. Elle sera pourtant exécutée et le DOM sera parcouru pour tenter de trouver ces éléments.

Multipliez ça par le nombre de queries inutiles que vous exécutez, et vous aurez une idée des problèmes de performance de votre codebase javascript.

Bien entendu, le problème ne se poserait pas si on pouvait demander à avoir un seul élément ou null. On aurait d'abord des erreurs "Cannot call method 'foo' of null". Puis on écrirait :

var $registration_form = document.querySelector( '#registration_form' );
if ( $registration_form ){
  // code
}

Très vite, on trouverait ça pénible que de devoir tester la présence à chaque fois, et on aurait l'idée sublime de ... s'assurer que ce code n'est exécuté que sur les pages où l'élément est présent. Mais jQuery n'encourage pas cela, il encourage la multiplication des queries exécutées sur toutes les pages. C'est cela la pollution.

Heureusement, ce problème est contournable, pourvu qu'on l'ait identifié. Quand j'utilise jQuery (quand j'utilise Mootools également, en fait), j'utilise un pattern d'auto-initialisation. Vous précisez sur un block quel module vous voulez initialiser, vous créez une classe du nom de ce module, et vous ne parcourez le DOM qu'une fois pour trouver les block qui demandent une initialisation. Si vous n'êtes pas à l'aise avec les prototypes, pas besoin d'écrire une classe dans les règles de l'art : une simple fonction appelée sans new peut suffire.

Soit le html :

<div id="admin_user_list" data-module="AdminUserList">
  <ul id="users">
    <li class="user"><span class="name">John Doe</span> <a class="delete">Delete</a></li>
    <li class="user"><span class="name">Pierre Dupont</span> <a class="delete">Delete</a></li>
    <li class="user"><span class="name">Sarah Connor</span> <a class="delete">Delete</a></li>
  </ul>
</div>

Et le javascript :

App.AdminUserList = function( $root ){
  $root.find( 'a.delete' ).click( this.userDeletion );
  // loads of queries to do stuff
};

App.AdminUserList.prototype.userDeletion = function(){ // ... };

$(function(){
  $( '*[data-module]' ).each( function( i, block ){
    $block = $(block);

    new App[ $block.attr( 'data-module' ) ]( $block );
  });
});

De cette manière, la seule query exécutée qui ne soit propre à votre page est $('*[data-module]'). Tout le reste ne sera exécuté que si vous en avez besoin.

Who need stuff like "caching" ?

Je ne sais pas si vous vous souvenez, mais Twitter, il y a quelques années, a eu un gros problème de performances pendant quelques jours. Après inspection, ils se sont rendu compte que cela venait d'une query DOM exécutée sur un scroll event. Il avait quelque chose de ce genre (exemple fictif):

$(window).scroll( function(){
  if ( $( '#notification' ).attr( 'data-new' ) ){
    // ...
  }
});

Vous voyez le problème ? À chaque scroll, le DOM est parcouru pour trouver #notification. Régler le problème est aussi simple que ça :

var $notification = $( '#notification' );

$(window).scroll( function(){
  if ( $notification.attr( 'data-new' ) ){
    // ...
  }
});

En fait, la seule raison pour laquelle on voudrait parcourir le DOM à nouveau serait la possibilité que #notification ait été supprimé et qu'un autre élément #notification ait été créé à la place. Autant dire que 99% du temps, on bénificierait d'avoir du caching par défaut.

Une implémentation qui ne pousserait pas à l'erreur serait de permettre de faire :

$( '#notification' ); // cache le résultat
$( '#notification' ); // retourne le résultat précédent, sans reparcourir le DOM
$( '#notification', { cache: false }); // parcour le DOM systématiquement

Pour être honnête, ce n'est pas ici un problème propre à jQuery : toutes les libs wrappant querySelector* fonctionnent ainsi, et c'est le comportement même de querySelector.

Là où ça me pose plus problème, c'est que $() ne mets pas clairement en évidence le fait qu'on va hitter le DOM, contrairement à document.getElements() et qu'il est beaucoup trop simple à écrire pour ne pas être gratuit. Écrivez cinq fois document.getElement( '#notification' ), vous allez rapidement faire ça :

var $notif = document.getElement( '#notification' );

Et c'est une bonne chose.

Encore une fois, rien d'incontournable avec jQuery, on peut très bien faire var $notif = $( '#notification' );. Mais encore faut-il connaître le problème. Faute de cela, le comportement qui sera encouragé sera justement de ne rien mettre en cache. Ici comme ailleurs, jQuery pousse les débutants au crime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.