WordPress et les JavaScripts asynchrones

Voici une optimisation simple à mettre en place et qui peut améliorer considérablement le temps de chargement de vos sites : différer le chargement des fichiers javascripts.

En règle générale, les javascripts sont synchrones, c’est à dire que lorsqu’une page web se charge et tombe sur une URL de fichier JS, le cacul de la page est suspendu, le temps de le télécharger.

On se retrouve donc sur certains sites WordPress avec une dizaine de librairies et de scripts qui viennent ralentir le chargement et l’affichage.

L’astuce dont je vais parler consiste à rendre tous les javascript appelés par WordPress « asynchrones » : Ils seront chargés parallèlement au reste de la page.

Cette méthode vous fera gagner en performance, donc en expérience utilisateur et potentiellement en référencement.

Différez le chargement des fichiers javascripts sur WordPress
WP Deferred Javascripts : un plugin pour charger vos JS de façon asynchrone

La librairie LabJS

Pour charger des javascripts de façon asynchrone, il faut utiliser une librairie de chargement de fichier JS.

LabJS en est une, comme il en existe d’autres, mais celle-ci pèse 640 bits avant gzippage.

Cela veut dire qu’au lieu de charger :

… pour parfois plusieurs centaines de Kbits au chargement de la page, votre site ce contentera de charger :

Vos javascripts seront alors importés indépendamment du reste de la page.
Et là c’est comme les légumes : ça compte pas…

Mise en place

Pour appliquer cette astuce, la procédure est de modifier la variable de WordPress qui contient la file de javascript, avant que le CMS n’essaye d’imprimer les fichiers.

Nous allons prendre les infos qu’elle contient, et les insérer dans une autre variable globale qui nous servira plus tard, à afficher les scripts ajoutés à la queue, comme bon nous semble.

Écouter la variable globale $wp-scripts

La variable qui contient toutes les infos relatives au JS sur WordPress s’appelle $wp_scripts. Elle est un objet de la classe du même  nom et WordPress l’utilise par le biais des fonctions regroupées dans le fichier functions.wp-scripts.php.

Avant toute chose, à l’init de WordPress, nous allons initialiser une variable $mes_scripts dans laquelle nous injecterons toutes les données qui nous intéressent.

add_action( 'init', 'initialize_script_global' );
function initialize_script_global() {
  global $mes_scripts;
  $mes_scripts = array();
}

Ensuite, avant que WordPress n’affiche les scripts, il faut que nous regardions le contenu du tableau queue de $wp_scripts.

Prenons ces infos et insérons-les dans notre variable perso.

// On se greffe sur wp_header et sur wp_footer...
// ... avant que wp_print_script ne soit exécuté...
// ... sinon ce sera trop tard, il n'y aura plus de hook pour empêcher...
// ... l'impression des scripts
add_action( 'wp_head', 'je_veux_mes_scripts', 1 );
add_action( 'wp_footer', 'je_veux_mes_scripts', 1 );
function je_veux_mes_scripts() {
  global $mes_scripts,$wp_scripts;
  foreach( $wp_scripts->queue as $s ){
    $mes_scripts[ $wp_scripts->registered[ $s ]->handle ] = array(
      'src'   => $wp_scripts->registered[ $s ]->src, // src du script
      'deps'  => $wp_scripts->registered[ $s ]->deps, // dépendances du script
      'extra' => $wp_scripts->registered[ $s ]->extra //données relatives (variables)
      );
  }
}

Nous venons d’obtenir la liste des scripts mis à la file, tout au long du chargement de WordPress, ainsi que toutes les infos qui leur sont relatives…

Empêcher l’affichage normal des scripts

Pour empêcher que WordPress ne vienne lui-même imprimer les scripts, il suffit simplement de vider le tableau queue de l’objet $wp_script.

Dans le header et le footer, après avoir ajouté nos scripts dans $mes_scripts (donc avant de fermer la fonction je_veux_mes_scripts()), il faut ajouter le code suivant :

$wp_scripts->queue = array();

WordPress lorsqu’il exécutera wp_print_script(), ne verra aucun fichier dans la file, et ne fera rien du tout.

Gérer les dépendances des JavaScripts

Si à ce stade nous regardons notre variable globale, nous nous apercevront que les scripts sont bien dedans, mais chargés dans n’importe quel ordre.

Nous ne pouvons pas lancer le rendu du JS directement, il faut d’abord nous inquiéter des dépendances. Dans le cas on nous omettrions cette étape, nous risquerions d’obtenir de nombreuses erreurs de javascripts (et accessoirement le site ne fonctionnerai plus^^)

Et là on se dit :

Mais comment fait WordPress pour gérer ça ?

La technique employée par WordPress est de faire une boucle pour trier les scripts :

  1. Les scripts sans aucune dépendances sont chargés directement.
  2. Ceux qui ont besoin d’une dépendance déjà instanciée peuvent se charger
  3. Ceux qui attendent leur dépendances feront un deuxième tour (ou plus)
  4. Ceux qui requiert un script qui n’est pas dans la file feront aussi un autre tour, mais demandent l’instanciation des scripts dont ils dépendent
  5. Ceux qui ont des dépendances qui n’existent nul part crèvent en silence…
  6. Lorsque tous les cas sont traités, la boucle prend fin.

La méthode peut paraître bourrin au premier abord, mais en fait il n’y a pas vraiment de solution mieux adaptée.

Il faut créer une fonction, qui va être hookée sur wp_footer, dans laquelle nous allons trier les javascripts. C’est dans cette même fonction que nous rendront le JS par la suite. Elle doit être exécutée assez tardivement ; il faut lui attribuer une priorité assez élevée.

Les outils que nous allons utiliser sont :

Au fur et a mesure de la boucle nous allons également avoir besoin de vider notre variable globale, il nous faut donc pour finir, une dernière variable $souvenir_de_mes_scripts qui sera une simple copie de $mes_scripts.

add_action( 'wp_footer', 'afficher_moi_tout_ca', 99 );
function afficher_moi_tout_ca() {
  global $mes_scripts,$wp_scripts;
  $i = count( $mes_scripts );

  $mes_scripts_ordonnes = array();
  $souvenir_de_mes_scripts = $mes_scripts;

  //la boucle infinie et le foreach sur la globale
  while( $i > 0 ){
    foreach( $mes_scripts as $k => $s ){
      //traitons nos scripts ici
    }
  }
}

Pour commencer, les scripts sans dépendances sont ajoutés directement.

if( empty( $s[ 'deps' ] ) ){
  $mes_scripts_ordonnes[ $k ] = $s;
  unset( $mes_scripts[ $k ] );
  $i--;
}

Ceux qui en ont font l’objet d’un traitement particulier, il ne seront ajoutés que si toutes leurs dépendances ont déjà été vues.

else{
  $alldepscounter = 0; //nombre de conditions remplies
  foreach( $s[ 'deps' ] as $d ){
    if( ! array_key_exists( $d, $souvenir_de_mes_scripts ) ) { //je n'aurais jamais ma dépendance...
      if( isset( $wp_scripts->registered[ $d ] ) ) { //... à moins qu'il y ai un script ?
        $mes_scripts[ $wp_scripts->registered[ $d ]->handle ] = array(
          'src'   => $wp_scripts->registered[ $d ]->src,
          'deps'  => $wp_scripts->registered[ $d ]->deps, 
          'extra' => $wp_scripts->registered[$d]->extra);
        $souvenir_de_mes_scripts[ $wp_scripts->registered[ $d ]->handle ] = $mes_scripts[ $wp_scripts->registered[ $d ]->handle ];
      }else{ //retourne dans ton monde, créature démoniaque...
        unset( $mes_scripts[ $k ] );
        $i--;
        break; 
      }
    }else{
      if( array_key_exists( $d, $mes_scripts_ordonnes ) ) { // je rempli une condition en +
        $alldepscounter++;
      }
    }
  }
  if( $alldepscounter == count( $s[ 'deps' ] ) ) { //bravo, toutes les conditions sont remplies
    $mes_scripts_ordonnes[ $k ] = $s;
    unset( $mes_scripts[ $k ] );
    $i--;
  }
}

Lorsque la globale est vide, alors la boucle est rompue et tous les scripts sont ordonnés dans notre nouvelle variable.

Il ne reste plus qu’à appeler LabJS et lui indiquer les fichiers à charger.

Charger les scripts

Voilà, maintenant il est temps de tout afficher, à condition qu’il y ai des scripts, bien sûr !

Donc si $mes_scripts_ordonnes n’est pas vide, alors on ajoute la librairie LabJS, puis on ouvre une balise script.

Dans cette balise script il suffit de boucler sur $mes_scripts_ordonnes et pour chacun, afficher leurs datas (issues de wp_localize_script()) puis leur sources :

// code à insérer avant la fermeture de affichez_moi_tout_ca()
if( ! empty( $mes_scripts_ordonnes ) ) {
  $output = '<script src="' . get_bloginfo( 'template_url' ) . '/js/lab.min.js"></script>' . "\n";
  $output.= '<script>';
  foreach( $mes_scripts_ordonnes as $s )
    $output.= $s[ 'extra' ][ 'data' ] . '$LAB.script("' . $s[ 'src' ] . '");' . "\n";
  $output.= '</script>';
  echo $output;
}

Le plugin WP Deferred Javascripts

Le code est complet et l’astuce devrait maintenant être opérationnelle, sauf dans certains cas :

Voici le code dans son intégralité :

//init global
add_action( 'init', 'initialize_script_global' );
function initialize_script_global() {
  global $mes_scripts;
  $mes_scripts = array();
}

//remplissage de $mes_scripts et vidage de $wp_scripts->queue
add_action( 'wp_head', 'je_veux_mes_scripts', 1 );
add_action( 'wp_footer', 'je_veux_mes_scripts', 1 );
function je_veux_mes_scripts() {
  global $mes_scripts,$wp_scripts;
  foreach( $wp_scripts->queue as $s ){
    $mes_scripts[ $wp_scripts->registered[ $s ]->handle ] = array(
      'src'   => $wp_scripts->registered[ $s ]->src, // src du script
      'deps'  => $wp_scripts->registered[ $s ]->deps, // dépendances du script
      'extra' => $wp_scripts->registered[ $s ]->extra //données relatives (variables)
      );
  }
  $wp_scripts->queue = array();
}

//tri et impression des scripts
add_action( 'wp_footer', 'afficher_moi_tout_ca', 99 );
function afficher_moi_tout_ca() {
  global $mes_scripts, $wp_scripts;
  $i = count( $mes_scripts );

  $mes_scripts_ordonnes = array();
  $souvenir_de_mes_scripts = $mes_scripts;

  //la boucle infinie et le foreach sur la globale
  while( $i > 0 ){
    foreach( $mes_scripts as $k => $s ){
      if( empty( $s[ 'deps' ] ) ){
        $mes_scripts_ordonnes[ $k ] = $s;
        unset( $mes_scripts[ $k ] );
        $i--;
      }else{
        $alldepscounter = 0; //nombre de conditions remplies
        foreach( $s[ 'deps' ] as $d ){
          if( ! array_key_exists( $d, $souvenir_de_mes_scripts ) ) { //je n'aurais jamais ma dépendance...
            if( isset( $wp_scripts->registered[ $d ] ) ) { //... à moins qu'il y ai un script ?
              $mes_scripts[ $wp_scripts->registered[ $d ]->handle ] = array(
                'src'   => $wp_scripts->registered[ $d ]->src,
                'deps'  => $wp_scripts->registered[ $d ]->deps, 
                'extra' => $wp_scripts->registered[ $d ]->extra);
              $souvenir_de_mes_scripts[ $wp_scripts->registered[ $d ]->handle ] = $mes_scripts[ $wp_scripts->registered[ $d ]->handle ];
            }else{ //retourne dans ton monde, créature démoniaque...
              unset( $mes_scripts[ $k ]);
              $i--;
              break; 
            }
          }else{
            if( array_key_exists( $d, $mes_scripts_ordonnes ) ) { // je rempli une condition en +
              $alldepscounter++;
            }
          }
        }
        if( $alldepscounter == count( $s[ 'deps' ] ) ) { //bravo, toutes les conditions sont remplies
          $mes_scripts_ordonnes[ $k ] = $s;
          unset( $mes_scripts[ $k ] );
          $i--;
        }
      }
    }
  }

  //print this
  if( ! empty( $mes_scripts_ordonnes ) ) {
    $output = '<script src="' . get_bloginfo( 'template_url' ) . '/js/lab.min.js"></script>' . "\n";
    $output.= '<script>';
    foreach( $mes_scripts_ordonnes as $s )
      $output.= $s[ 'extra' ][ 'data' ] . '$LAB.script("' . $s[ 'src' ] . '");' . "\n";
    $output.= '</script>';
    echo $output;
  }
}

Vu qu’il s’agit d’une optimisation assez avancé, l’intérêt n’est pas de recoder ça sur chaque site. Daniel Roch et moi en avons donc fait un plugin qui est disponible sur l’extend de WordPress : WP Deferred Javascripts (actuellement en version 1.5.6).

Si vous avez quoi que ce soit comme soucis relatif au plugin, je vous invite à nous faire remonter les bugs via l’onglet « Support » de la page du plugin.

Alors, quel est maintenant votre ressenti sur le gain du temps de chargement lié à ce plugin ?

19 commentaires

  1. Par Julio Potier (BoiteAWeb.fr) — Il y a 1 année

    Ca à l’air super ça, je teste sur ma v3 et sur secupress, promis ;)
    Merci pour ce premier plugin, d’autres prévus ? ;)

  2. Par Pierre — Il y a 1 année

    Ça m’a l’air pas mal du tout ^^, je l’ai mise en production sur 3 sites pour des tests pour voir le gain de perfs …

  3. Par Willy Bahuaud — Il y a 1 année

    Merci !! En ce qui concerne les autres plugins, j’en ai fait un il y a quelques semaines (et faudrait que je fasse un article dessus pour expliquer son utilité) : Multi image metabox

    Sinon j’ai un autre plugin sur le feu depuis plus de deux ans… faudrait que je le termine… ;-P

  4. Par Daniel Roch — Il y a 1 année

    Content de voir que notre plugin plaise en tout cas. L’autre truc bien d’ailleurs avec lui, c’est que l’on voit tout de suite les plugins et les thèmes mal codés…

    : si tu trouves le plugin performant, n’hésite pas à faire une review ici.

    : deux ans, tu es large encore ^^

  5. Par Aurélien Denis — Il y a 1 année

    Bon… va falloir que j’essaie même si j’ai pas tout compris de l’article ! :D

  6. Par Julien Maury — Il y a 1 année

    Excellent, m’a permis de débuguer mon js qui plus est. J’ai pu constater le gain en mode admin (pas de cache).

  7. Par Morgan — Il y a 1 année

    Pas eu le courage de lire jusqu’au bout après les quelques articles que je me suis tapé sur d’autres blogs, mais ce que j’ai pu lire m’a plu. Je repasserai par là pour essayer ce plugin et voir ce qu’il vaut.

  8. Par Arnaud — Il y a 1 année

    Du bon boulot, je vais regarder pour mettre ça en place.
    En plus le chargement du Javascript prend pas mal de place pour mon site, et ça n’a pas l’air compliqué à mettre en place.

  9. Par Fabrice — Il y a 1 année

    Excellent et je m’en vais ci-après ce commentaire, essayer le plugin…

    Thanks Willy & Daniel, c’est mon indice Google speed qui va être content ;)

    Ps/ Willy, j’ai essayé ta 404…. une tuerie !!!

  10. Par GeekPress — Il y a 1 année

    Je suis septique sur un éventuel gains de temps sur un site qui contient un unique js et minifié. J’ai même lu que ça pouvait ralentir le chargement du script !

    Par contre,je pense que ce plugin est plutôt dédié aux thèmes usine à gaz qui mettent 40 fichiers js dans le footer.

  11. Par Willy Bahuaud — Il y a 1 année

    , , Oui, essayez et dites-moi ce que vous en pensez. Il se peut qu’il ai subsisté quelques bug jusqu’à aujourd’hui. Maintenant il semble me semble beaucoup plus stable.

    , Oui en mode Admin on se rend vite compte. Si en revanche on a un front bien optimisé avec un seul JS et un bon système de cache, je ne sais pas trop si on sent une différence.

    Je suis content que la 404 te plaise ! Et encore elle n’est pas comme je l’aurais voulu…

    Un jour mon plugin il sortira, un jour… (tu fais quoi vendredi en 8 ? Une petite coding night ?)

    Si vous connaissez quelques sites bien optimisés par défaut, pour y faire un jsPerf avec et sans le plugin, je suis preneur !

  12. Par Alexandre Ionoff — Il y a 1 année

    Merci pour ce plugin, je l’ai mis en prod sur plusieurs sites et ça fonctionne pas mal en effet.

  13. Par ouvrela — Il y a 1 année

    Bonjour,
    j’ai installé votre plugin qui fonctionne fort bien mais depuis quelques jours j’ai changé de thème et maintenant j’ai un souci.C’est un thème responsive gratuit pour infos.Dans ce thème il y a un slider et si j’active le plugin celui-ci n’affiche plus les images,en désactivant le plugin , le slider re fonctionne?
    Peut-être un conflit de scripts ? mauvais code css?
    Auriez-vous une idée , une piste ?
    merci
    cordialement

  14. Par Yoav — Il y a 12 mois

    Excellent ! Combiné à Incapsula.com, WP Cache Machine et WP Defer Javascript; c’est une fusée ! Recommandé :)

  15. Par eslovenie — Il y a 11 mois

    Hello,

    sur mon site, votre plugin fait bugger les commentaires G+ avec ce plugin : Google+ Comments

  16. Par Phil — Il y a 11 mois

    Bonjour,

    très content de la première version avec une amélioration très net du temps de chargement , fonctionnnant bien avec minify.
    Parcontre depuis les dernère mise à jour, j’ai constaté des ralentissements notoire au point que j’ai désactivé le plug in et décoché  » Enable JavaScript Minification » dans minify pour que Lightview Plus puisse fonctionner.
    voilà quelques détails , si cela peut vous aider…
    Phil

  17. Par Rosa — Il y a 10 mois

    J’ai une petite question, si je le mets (ton plugin) sur mon blog, est-ce que je peux enlever assetsminify ?

  18. Par Jeff — Il y a 7 mois

    Je bloquais sur des erreurs javascript à la suite de l’installation de plusieurs plugin sur mon thème qui en contient déjà pas mal. Résultat mon site était bloqué.
    Avec ce plugin, plus d’erreur. !

  19. Par Andre — Il y a 6 mois

    Hello,
    Chez moi cette extension empêche « Lightbox Plus Color » de fonctionner. J’aimagine que le javascript ne se charge pas correctement?
    ++ andre

Commenter