Augmenter la vitesse d'affichage des pages web : minification, mise en cache et exécution différée du JavaScript

Magazine
Marque
GNU/Linux Magazine
Numéro
156
|
Mois de parution
janvier 2013
|
Domaines


Résumé
Face à des contraintes de vitesse toujours plus élevées (notamment avec l'arrivée des smartphones) et des interfaces client toujours plus riches (pratiquement 100 000 caractères pour la version « minifiée » du framework JavaScript jQuery), plusieurs techniques permettent de nettement améliorer la vitesse de chargement des pages web.

Body

1. Introduction

Imaginons une page web contenant un grand nombre d'informations, par exemple une longue liste de produits sur un site de VPC. À côté de chaque produit se trouvent des boutons permettant diverses opérations : ajouter au panier, visualisation détaillée, etc. Rapidement, on se retrouve avec un site pourvu de nombreuses fonctionnalités, et donc une bibliothèque JavaScript assez riche, souvent basée sur un framework efficace mais volumineux tel que jQuery. Le problème est alors le suivant : tant que le navigateur n'a pas terminé de charger et d'interpréter l'intégralité du JavaScript nécessaire aux fonctionnalités de la page, l'utilisateur ne peut pas profiter de son contenu « brut », qui est pourtant ce qui l'intéresse.

Mettons en œuvre ces « mauvaises pratiques » sur un exemple minimaliste, peu contraignant en termes de vitesse d'exécution de la page, mais néanmoins assez révélateur. Pour cela, téléchargeons la dernière version du framework jQuery (version 1.8.2 au moment de la rédaction de cet article) sur le site de jQuery [1], ce qui nous donne le fichier jquery-1.8.2.js, et créons deux autres fichiers : un fichier HTML (example00.html) et un fichier JavaScript (example00.js).

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">

  <head>

  <title>Un exemple de pages a problemes</title>

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

  <script type="text/javascript" src="./jquery-1.8.2.js"></script>

  <script type="text/javascript" src="./example00.js"></script>

  </head>

  <body>

    <div id="divLoading">Chargement en cours...</div>

  </body>

</html>

(function (window) {

    function ready (msg) {

        if (typeof (msg) == "string") {

            $("#divLoading").html (msg);

        }

    }

    $(window.document).ready (function () {

        ready ("Document "+ window.location.href +" pret !");

    });

}) (window);

Notons l'emploi d'une fonction anonyme dans le fichier JavaScript, qui permet de définir une sorte de namespace où l'on peut définir des variables internes sans crainte de collisions (elle ne seront pas exportées dans le reste du code JavaScript de la page). La fonction ready, quant à elle, n'a pas été inlinée et ce dans un but purement pédagogique, comme nous le verrons plus tard. Un outil d'analyse du Web quelconque (Google PageSpeed [2], Yahoo! YSlow [3], etc.) permet alors de dresser le diagnostic suivant : le temps de chargement moyen du document est supérieur à 450 ms sur notre machine d'essai. Bien trop pour un si petit exemple !

Procédons à une analyse plus fine. Parmi ces 450 ms, il faut environ 150 ms pour charger et afficher le HTML de la page, environ 120 ms pour envoyer les requêtes HTTP GET vers les deux fichiers JavaScript et les recevoir, et plus environ 180 ms pour les interpréter. La partie compressible se situe donc au niveau de ces deux derniers intervalles de temps : il est possible d'agir sur les délais de réception et d'interprétation.

2. Minification des fichiers JavaScript

Jetons un coup d'œil au fichier jquery-1.8.2.js. Il comprend beaucoup d'espaces, de lignes vides, de commentaires, des noms de variables, de fonctions et d'arguments explicites... Autant de choses utiles au développement et au debugging, mais totalement inutiles à l'interpréteur JavaScript du navigateur client. De nos jours, il existe plusieurs outils permettant de « minifier » des fichiers JavaScript (et même CSS), de façon à les réduire au strict minimum nécessaire à leur interprétation. Parmi ces outils, on peut notamment citer Google Closure [4], Yahoo! YUI Compressor [5], ou encore UglifyJS [6]. Chacun possède ses avantages et ses défauts, ainsi qu'un nombre de fonctionnalités propres. Dans cet article, nous nous concentrerons sur Closure, qui permet de nombreuses optimisations allant au-delà de la minification, telles que l'inlining de fonctions, par exemple.

Commençons par minifier jQuery. En l'occurrence, les développeurs s'en sont chargés pour nous, et proposent une version déjà minifiée (par UglifyJS), pour laquelle on passe de 266 882 à 93 436 caractères, soit un gain de près de 65% ! Récupérons-la sur le site web de jQuery, et enregistrons-la dans le fichier jquery-1.8.2.min.js.

Allons maintenant plus loin en compressant également notre fichier example00.js, bien que sa petite taille (peu réaliste néanmoins) fera qu'on n'y gagnera pas grand chose.

$ closure --charset UTF-8 --warning_level QUIET --compilation_level ADVANCED_OPTIMIZATIONS --externs jquery-1.8.2.js example00.js

var a=window;$(a.document).ready(function(){var b="Document "+a.location.href+" pret !";"string"==typeof b&&$("#divLoading").html(b)});

Nous avons utilisé ici plusieurs options de compilation. La première, charset, permet de définir l'encodage d'entrée et de sortie, ici UTF-8. warning_level permet d'éviter quelques warnings liés au code de jQuery que Closure ne semble pas particulièrement apprécier... Mais il faut s'assurer que son propre code est « propre » avant de s'en servir. compilation_level permet de choisir le niveau de compilation parmi trois niveaux : WHITESPACE_ONLY (on se contente de supprimer les commentaires, les espaces inutiles, et les retours à la ligne), SIMPLE_OPTIMIZATIONS (on renomme aussi le nom des fonctions, variables et arguments de façon à les raccourcir), et ADVANCED_OPTIMIZATIONS (qui permet des optimisations plus agressives, telles que l'inlining). Et enfin, externs permet de spécifier sur quels autres fichiers JavaScript s'appuie notre fichier, en l'occurrence jQuery, de manière à ne pas minifier les noms des fonctions appelées de ce dernier, comme la fonction $.ready.

Comme on peut le constater, Closure a grandement simplifié notre fichier, et a même inliné la fonction ready. Le gain est assez faible sur un aussi petit fichier, mais on peut imaginer le gain en taille et en vitesse d'exécution sur un fichier de plusieurs milliers de lignes ! Voyons maintenant l'effet sur le temps de chargement de notre premier exemple. Le temps moyen de chargement tombe à environ 380 ms, le gain s'effectuant sur le temps de chargement (environ 70 ms) et le temps d'interprétation (environ 160 ms) des deux fichiers JavaScript.

Ces résultats sont très encourageants, et seraient d'autant plus importants que le JavaScript employé serait complexe. La même logique peut d'ailleurs s'appliquer aux fichiers CSS en utilisant un autre compilateur (Closure ne sait pas le compiler), tel que Yahoo! YUI Compressor ou CSSO [7].

Enfin, terminons cette section par deux astuces. La première : que faire lorsque l'on veut que la fonction ready soit exportée, de manière à pouvoir l'utiliser dans le code HTML, par exemple avec un <a href="javascript: void (0);" onclick="ready ();" /> ? Il existe une astuce pour cela, qui consiste à modifier notre fichier exemple00.js de la manière suivante :

(function (window) {

    function ready (msg) {

        if (typeof (msg) == "string") {

            $("#divLoading").html (msg);

        }

    }

/* Export symbol. */

window ["ready"] = ready;

    $(window.document).ready (function () {

        ready ("Document "+ window.location.href +" pret !");

    });

}) (window);

Le résultat produit par compilation est alors le suivant :

var a=window;function b(c){"string"==typeof c&&$("#divLoading").html(c)}a.ready=b;$(a.document).ready(function(){b("Document "+a.location.href+" pret !")});

Imaginons maintenant que l'on doive charger plusieurs fichiers JavaScript, et qu'on les compile tous. Il y a alors un risque de collision très élevé entre ces différents fichiers, du fait des variables globales exportées avec des noms très courts, tels que a, b... Il existe cependant un moyen de « forcer » à ne pas inliner la fonction anonyme qui englobe tout notre fichier, grâce à l'option de compilation output_wrapper :

$ closure --charset UTF-8 --warning_level QUIET --compilation_level ADVANCED_OPTIMIZATIONS --externs jquery-1.8.2.js --output_wrapper '(function(window){%output%})(window);' example00.js 2>/dev/null

(function(window){var a=window;$(a.document).ready(function(){var b="Document "+a.location.href+" pret !";"string"==typeof b&&$("#divLoading").html(b)});})(window);

Poussons même le bouchon un peu plus loin, en compilant le résultat obtenu au niveau de compilation SIMPLE_OPTIMIZATIONS, qui ne va pas inliner la fonction anonyme mais qui va renommer son argument et supprimer la variable intermédiaire a inutile :

(function(a){$(a.document).ready(function(){var b="Document "+a.location.href+" pret !";"string"==typeof b&&$("#divLoading").html(b)})})(window);

Avant de passer à la suite, sauvegardons ce résultat dans un fichier example02.js.

3. Mise en cache

La plupart de ces fichiers JavaScript sont statiques, c'est-à-dire qu'ils ne sont pas amenés à changer, sauf cas exceptionnels (maintenance, développements, etc.). Cela peut donc rendre de grands services de confier le soin au navigateur d'en sauvegarder une copie sur le disque dur, afin de s'épargner le temps d'émettre une requête HTTP GET et de récupérer les fichiers depuis Internet.

Une manière simple de procéder est d'utiliser le module headers du serveur web Apache, que l'on active tout simplement en exécutant (en root) :

# a2enmod headers

Il suffit alors de créer un fichier .htaccess à la racine du site web, comprenant :

<ifModule mod_headers.c>

    <filesMatch "\.css$">

        Header set Cache-Control "max-age=604800, public"

        Header set Content-type "text/css; charset=utf-8"

    </filesMatch>

    <filesMatch "\.js$">

        Header set Cache-Control "max-age=604800, public"

        Header set Content-type "application/javascript; charset=utf-8"

    </filesMatch>

</ifModule>

Ici, le délai d'expiration (max-age) est mis à une semaine (604800 secondes), et peut être ajusté selon les besoins. Cela signifie que, lors d'une première visite, le navigateur client va récupérer les fichiers CSS (.css) et JavaScript (.js) depuis l'Internet, et va les stocker temporairement sur le disque dur pendant une semaine. Lors de tout accès futur dans ce délai, les fichiers en question seront récupérés depuis le disque dur de l'ordinateur client plutôt que depuis l'Internet, écourtant considérablement les délais de chargement.

Immédiatement, le temps moyen de chargement de la page descend à 320 ms, le gain s'étant effectué sur le temps de chargement des fichiers JavaScript.

4. Exécution différée du JavaScript

Revenons à l'exemple du site de VPC mentionné en introduction. Comme nous l'avons vu, il n'est pas nécessaire d'attendre que tous les fichiers JavaScript soient chargés et interprétés pour commencer à naviguer la page. Il est donc utile de différer l'exécution du JavaScript après l'interprétation du HTML.

Il nous faudra alors composer avec deux choses : d'une part, la capacité d'un navigateur à charger plusieurs fichiers JavaScript en parallèle et, d'autre part, les dépendances qu'il peut y avoir entre ces fichiers. Dans notre cas, notre fichier example_02.js dépend entièrement du fichier jquery-1.8.2.min.js et ne peut être exécuté sans que ce dernier ait été interprété. Il convient donc de concevoir un système de dépendances entre les scripts.

Considérons l'exemple de fichier HTML suivant, qui inclut un tel système :

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">

  <head>

  <title>Un exemple de pages a problemes</title>

  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

  <script type="text/javascript">

    /* <![CDATA[ */

      (function (window) {

          var scripts = [["jquery-1.8.2.min.js"],["example02.js"]];

          function load_scripts (scripts, callback) {

              var count = scripts.length;

              for (var j = 0; j < count; j++) {

                  if (typeof (scripts [j]) == "string") {

                      var url = scripts [j];

                      var script = window.document.createElement ("script")

                      script.type = "text/javascript";

                      if (script.readyState) {

                          script.onreadystatechange = function () {

                              if (script.readyState == "loaded" || script.readyState == "complete") {

                                  script.onreadystatechange = null;

                                  if (j == count) {

                                      callback ();

                                  }

                              }

                          };

                      } else {

                          script.onload = function () {

                              if (j == count) {

                                  callback ();

                              }

                          };

                      }

                      script.src = url;

                      window.document.getElementsByTagName ("head")[0].appendChild (script);

                  }

              }

          }

          function parse_scripts (scripts, i) {

              if (typeof (scripts [i]) == "object") {

                  load_scripts (scripts [i], function () {

                      parse_scripts (scripts, (i + 1));

                  });

              }

          }

          if (typeof (scripts) == "object") {

              parse_scripts (scripts, 0);

          }

      }) (window);

    /* ]]> */

  </script>

  </head>

  <body>

    <div id="divLoading">Chargement en cours...</div>

  </body>

</html>

Concentrons-nous sur la variable scripts du code JavaScript inclus au code HTML ci-dessus. Elle se présente sous la forme d'un tableau de tableaux, qui s'interprète de la manière suivante : tant que tous les fichiers de la variable scripts [i] n'ont pas été chargés, on ne tente pas de charger les fichiers de la variable scripts [i + 1]. Ainsi, si l'on avait deux fichiers JavaScript indépendants, a.js et b.js, un fichier c.js qui dépend de a.js, et un fichier d.js qui dépend de b.js et c.js, on aurait pu écrire la variable scripts sous la forme :

var scripts = [["a.js", "b.js"], ["c.js"], ["d.js"]];

Le reste du code fonctionne de la manière suivante : la fonction parse_scripts, qui prend deux arguments (la variable scripts et un index de tableau i), va charger tous les fichiers JavaScript dont le chemin se trouve dans les éléments du tableau scripts [i] par l'intermédiaire de la fonction load_scripts. Une fois tous ces fichiers chargés, un callback sur la fonction parse_scripts va permettre de charger tous ceux du tableau scripts [i + 1], et ainsi de suite jusqu'à ce que la variable scripts ait entièrement été parcourue.

Pour finir, minifions également le code JavaScript intégré au fichier HTML, selon les méthodes décrites précédemment. On obtient alors un temps de chargement moyen d'environ 160 ms, soit à peine 10 ms de plus que le temps nécessaire à interpréter le HTML seul. Le gain final approche les 65%, sur un exemple pourtant très réduit.

Conclusion

Les trois techniques que nous venons de voir (la minification, la mise en cache et l'exécution différée) permettent d'accélérer grandement le temps nécessaire avant qu'un utilisateur puisse naviguer un site web donné. Même si le gain absolu peut s'avérer faible pour les ordinateurs d'aujourd'hui, il n'est souvent pas négligeable sur les smartphones, entre autres.

Notre exemple de gestion des dépendances peut, par ailleurs, être couplé à un script PHP afin de pouvoir les générer de manière dynamique (ce qui est utile lorsque l'on utilise un système de templates), notamment en utilisant la fonction json_encode :

<?php

$javascript = array ();

$javascript [] = array ('a.js', 'b.js');

$javascript [] = array ('c.js');

$javascript [] = array ('d.js');

?>

var scripts = <?php json_encode ($javascript); /* produit [["a.js","b.js"],["c.js"],["d.js"]] */?>;

En cas de grande complexité de l'interface client, il est possible de pousser la logique encore plus loin, en chargeant « à la demande » les portions de JavaScript nécessaires, comme on peut le voir dans le code de Google GMail.

Références

[1] http://jquery.com/

[2] https://developers.google.com/speed/pagespeed/

[3] http://developer.yahoo.com/yslow/

[4] https://developers.google.com/closure/

[5] http://developer.yahoo.com/yui/compressor/

[6] https://github.com/mishoo/UglifyJS

[7] http://css.github.com/csso/


Sur le même sujet

Programmation UEFI

Magazine
Marque
MISC
Numéro
107
|
Mois de parution
janvier 2020
|
Domaines
Résumé

La deuxième étape de la compréhension du système UEFI va s’articuler autour de l’implémentation en C avec l’optique de fabriquer sa propre brique UEFI. Le développement va permettre d’illustrer le processus de la chaîne de boot avec pour objectif de mieux appréhender les différentes problématiques de sécurité.

Gestion de projets Python avec Pyenv et Pipenv : effet de mode ou solution efficace ?

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
106
|
Mois de parution
janvier 2020
|
Domaines
Résumé

Dans le cadre de développements Python, il y a deux éléments cruciaux : la gestion des environnements virtuels et la gestion des dépendances. Pour cela, il existe deux outils très efficaces : Pyenv et Pip. De plus en plus de développeurs substituent Pipenv à Pip et, en le couplant à Pyenv, présentent cela comme LA solution ultime ! Mais est-ce réellement le cas ?

Gérez, protégez et partagez vos mots de passe avec KeePassXC

Magazine
Marque
Linux Pratique
Numéro
117
|
Mois de parution
janvier 2020
|
Domaines
Résumé

Nous stockons de nombreuses informations, pour beaucoup sensibles, et dans des formats différents. Cela fait autant de mots de passe à créer, à retenir et à utiliser. À utiliser pour souvent quotidiennement, il faut donc que leur utilisation soit la plus transparente possible et s’adapte aux différents services clients : données sur une partition chiffrée, site internet, client d’une application bancaire, application en ligne de commandes. Vous utilisez peut-être déjà une extension web pour les sites web : c’est bien, mais cela ne gère pas tous vos besoins en mots de passe, mots de passe qui sont peut-être gérés par une société tierce sur leurs serveurs lorsque vous les rentrez dans l’extension. Dans cet article, nous allons découvrir KeePassXC, un gestionnaire de mots de passe libre qui vous permettra de répondre à tous types de besoins et de ne pas partager vos mots de passe avec une société tierce.

C++ Moderne : C++17 (partie 2)

Magazine
Marque
GNU/Linux Magazine
Numéro
233
|
Mois de parution
janvier 2020
|
Domaines
Résumé

Dans le précédent article sur C++ 17, nous avons abordé les évolutions du langage et les évolutions de la STL orientées sur les types de bases. Continuons aujourd'hui notre découverte de C++ 17 !

Par le même auteur

Augmenter la vitesse d'affichage des pages web : minification, mise en cache et exécution différée du JavaScript

Magazine
Marque
GNU/Linux Magazine
Numéro
156
|
Mois de parution
janvier 2013
|
Domaines
Résumé
Face à des contraintes de vitesse toujours plus élevées (notamment avec l'arrivée des smartphones) et des interfaces client toujours plus riches (pratiquement 100 000 caractères pour la version « minifiée » du framework JavaScript jQuery), plusieurs techniques permettent de nettement améliorer la vitesse de chargement des pages web.