Charger des fichiers javascript de façon performante

le 24/06/2011 par Eric Daspet
Tags: Software Engineering

Users really respond to speed

La citation est de Marissa Meyer, VP expérience utilisateur à Google, en 2006.

Pas grand chose n’a changé depuis, si ce n’est qu’on a des chiffres plus précis, et un peu effrayants, sur l’importance de la performance dans les applications web : Quelques points de performance feront la différence entre une expérience réussie et une application perçue négativement par ses utilisateurs.

Ou plutôt si, ce qui a changé c’est que depuis 2006 on ne se contente plus de sites web, les applications web ont envahi les entreprises et leur SI. Il devient donc primordial de faire attention aux spécificités des applications web : On ne traite pas un navigateur comme un client lourd.

Les spécificités des clients légers

Sur un client lourd c’est essentiellement le code applicatif qui va être un problème et qu’on va se charger d’optimiser : pools d’accès à la base de données, les index, les caches applicatifs, optimisation du code, ou même configuration des différents services et des serveurs.

Sur une application web, plus de 90 % du temps est passé sur le réseau et dans le navigateur. Sur les 10 plus gros sites web français on tourne même plutôt autour de 95 %. Ce qui nous préoccupera c’est donc l’enchaînement des requêtes http et l’architecture de la page.

Cascade réseau exemple

Agir sur l’applicatif sur le serveur c’est agir sur la petite barre verte de la première ligne de notre exemple, et uniquement celle-ci.

Cette première ligne représente une demie seconde sur les dix secondes et demie nécessaires à charger complètement notre écran web lors du premier accès.

Améliorer de juste 5 % le reste de notre écran sera plus efficace que de diviser par 4 ou 5 le temps de calcul sur le serveur.

Fort impact du JavaScript

Les applications web riches ont tendance à faire un usage excessif de JavaScript, et c’est encore plus vrai quand on tente de reproduire l’interface d’un client lourd habituel.

C’est souvent ce code JavaScript qui pose pourtant le problème de performance le plus visible. Dans notre exemple ce qui saute aux yeux c’est ce grand rectangle bleu en quatrième ligne. Cette quatrième ligne c’est le chargement de la bibliothèque jQuery, de ses modules, et des codes spécifiques à l’affichage de l’application.

Contrairement à ce qu’on pourrait penser, ce n’est pas l’exécution du code JavaScript qui est à optimiser, mais bien son téléchargement dans le navigateur. Tous les navigateurs parallélisent les téléchargements pour optimiser l’utilisation de la bande passante mais Microsoft Internet Explorer 6 et 7 ne savent pas le faire quand ils téléchargent du JavaScript.

Tout est alors mis en attente : non seulement les autres composants à télécharger mais aussi le rendu du reste de la page. C’est soit une page blanche soit la page précédente avec un sablier en guise de curseur que l’utilisateur aura comme seul retour. L’expérience est désastreuse en termes de ressenti utilisateur.

Extrait de la cascade réseau exemple

De la seconde 1,5 à la seconde 3,5, le navigateur ne fait rien d’autre que télécharger le code JavaScript. On voit pourtant que le navigateur ne fait pas grand chose : la courbe processeur (orange) et la courbe de bande passante (verte) sont loin d’être à leur maximum.

Si nous pouvions paralléliser le reste des activités du navigateur, nous pourrions gagner de une à deux secondes sur le chargement total de la page, soit 10 % à 20 % de performance.

Chargement asynchrone

Il existe plusieurs techniques pour faire sauter le comportement bloquant de JavaScript. Chacune a ses inconvénients et aucune ne couvre tous les cas d’utilisations.

Créer dynamiquement la balise <script src=’…’> avec le DOM

// à la place de <script src="/path/to/script.js"></script>

// on créé la balise <script> dynamiquement
// via le DOM et createElement
var s = document.createElement("script") ;
s.type = "text/JavaScript" ;
s.src = "/path/to/script.js" ;
s.async = false ;
// puis on l’insère dans la balise <head> en haut de document
var head = document.head || document.getElementsByTagName("head")[0] ;
head.appendChild(s) ;

Cette technique assure un chargement asynchrone sur tous les navigateurs mais les scripts s’exécuteront dans leur ordre d’arrivée et pas dans leur ordre d’appel sous Microsoft Internet Explorer, ce qui empêche de gérer des dépendances entre plusieurs scripts. Elle a toutefois le fort avantage de ne rien demander d’autre que ces quelques lignes.

C’est généralement cette technique qu’on utilise pour télécharger de manière asynchrone les scripts de publicité et les compteurs de visite.

Télécharger le script via une requête Ajax avant son insertion dans la page

// à la place de <script src="/path/to/script.js"></script>

// on initialise une requête Ajax
// pour télécharger le fichier javascript
var req ;
if (window.XMLHttpRequest) {
  req = new XMLHttpRequest();
} else if (window.ActiveXObject) {
  req = new ActiveXObject("Microsoft.XMLHTTP");
}
req.onreadystatechange = function() {
  if (req.readyState == 4 && req.status == 200) {
    // en cas de réussite on insère dynamiquement une balise <script>
    // avec le code javascript comme contenu
    var s = document.createElement("script") ;
    s.type = "text/JavaScript" ;
    s.appendChild(document.createTextNode(req.responseText) ;
    var head = document.head || document.getElementsByTagName("head")[0] ;
    head.appendChild(s) ;
  }
}
req.open("GET", "/path/to/script.js") ;
req.send(null) ;

Cette technique a l’avantage de vous dissocier l’exécution et le téléchargement, vous permettant de gérer vous-même les dépendances entre scripts, mais nécessite que la page HTML et le fichier JavaScript soient sur le même domaine. Le code de gestion des dépendances peut aussi être un peu long à concevoir.

Initialiser le cache navigateur avant insertion

// à la place de <script src="/path/to/script.js"></script>

// Une fois le fichier javascript téléchargé une fois (voir plus bas)
// il devrait se trouver dans le cache du navigateur
// et donc s’exécuter immédiatement quand on
// créé la balise <script> correspondante via DOM
var insertScript = function(url) {
  var s = document.createElement("script") ;
  s.type = "text/JavaScript" ;
  s.src = "/path/to/script.js" ;
  var head = document.head || document.getElementsByTagName("head")[0] ;
  head.appendChild(s) ;
}
// On fait télécharger le fichier en utilisant
// - soit un préchargement d’image (pour IE)
// - soit la balise <object> (pour les autres navigateurs)
// Le fichier sera donc téléchargé mais non exécuté
// La fonction insertScript est exécutée à la fin du téléchargement
var path = "/path/to/script.js" ;
if (navigator.appName.indexOf('Microsoft') === 0) {
  var o = new Image() ;
  o.onload = insertScript ;
  o.src = path ;
} else {
  var o = document.createElement("object") ;
  o.type = "text/cache" ;
  o.onload = insertScript ;
  o.data = "/path/to/script.js" ;
  o.width = 0 ;
  o.height = 0 ;
  document.body.appendChild(o) ;
}

Ici nous devons gérer différemment Opera et Microsoft Internet Explorer du reste des navigateurs. Si le navigateur ne met pas correctement en cache le fichier JavaScript, nous risquons aussi de générer deux requêtes HTTP, voire deux téléchargements, et dégrader très nettement les performances. En échange le téléchargement et l’exécution sont décorrélés et il n’y a pas de contrainte de domaine.

Plus complète mais plus complexe, c’est souvent cette technique qui est utilisée par les bibliothèques de code qui s’occupent de ces questions pour vous et vous mâchent le travail.

Tout ceci existe déjà

À nos exemples il faut ajouter la gestion des erreurs, des dépendances entre fichiers externes, ainsi les dépendances entre fichiers externes et scripts embarqués dans le HTML. Un tel code est complexe mais surtout il est fragile car il est facile de casser telle ou telle version d’un navigateur sans s’en rendre compte.

Heureusement d’autres l’ont fait pour nous. Il existe de nombreuses bibliothèques de code complètes qui proposent le chargement asynchrone des fichiers JavaScript. Les plus connues se nomment LABjs, Headjs, ControlJs et RequireJS.

Une fois payé le surcoût dû à la bibliothèque elle-même (1,5 à 2,2 Ko pour les trois premières), vous avez à votre disposition des méthodes pour charger des fichiers JavaScript de façon asynchrone sur l’ensemble des navigateurs en gérant les dépendances entre blocs de code et fichiers.

LABjs a un développement actif et son développeur principal met un point d’honneur à fouiller et vérifier tous les cas possible sur les différents navigateurs. C’est celle que nous vous recommandons si vous n’avez pas de besoin plus spécifiques.

Avec LABjs cela donne :

$LAB.script("jquery.js").wait()
  .script("jquery-moduleA.js")
  .script("jquery-moduleB.js").wait()
  .script("site.js")
  .wait( function() {
    doSomethingWithDocumentAndJquery() ;
  } ) ;

Les fichiers jquery.js, jquery-moduleA.js, jquery-moduleB.js et site.js seront tous les quatre téléchargés en parallèle. L’instruction wait() impose que tous les fichiers déjà en cours de chargement soient exécutés avant d’exécuter les fichiers suivants. Le fichier jquery.js sera donc toujours exécuté en premier et le fichier site.js sera toujours exécuté en dernier quand bien même le téléchargement serait fait dans un autre ordre (parce que jquery.js est plus volumineux donc plus long par exemple). La dernière instruction wait() permet d’introduire une dépendance pour un code directement embarqué dans la page HTML.

Quels résultats ?

Dans notre exemple, l’utilisation de LABjs impose des pages 2 Ko plus volumineuses mais permet d’exploiter deux files de téléchargement au lieu d’une seule pendant deux secondes.

Comme la bande passante est largement sous-utilisée lors d’une session de navigation, le surpoids de 2 Ko au total est quasiment invisible par rapport au gain d’une seconde sur le chargement de la page.

Le temps de travail est d’environ une journée pour bien faire les choses et convertir les fichiers externes et les quelques scripts directement embarqués dans la page à la syntaxe LAB.js. Pour diminuer le temps de chargement de la page d’une seconde, c’est un retour sur investissement quasiment imbattable.

Pour aller plus loin, il est aussi conseillé d’éviter de multiplier le nombre de fichiers JavaScript à télécharger. Au delà de deux, quatre au maximum, il est préférable de les regrouper entre eux. C’est d’autant plus vrai si votre application est accédée via des smartphones sur réseau 3G ou via des réseaux distants avec une forte latence.