Ce post est le second d’une série de 3 et est tiré de l’expérience des finalistes du concours de performance Web 2010

Nous allons analyser les stratégies et techniques gagnantes (ou perdantes parfois) de chargement des dépendances de la page (CSS, JS, images, XHR). C’est là dessus que se sont concentrés les finalistes car il n’existe rien de suffisamment universel pour espérer gagner ce concours, nous avons donc assisté un joli combat de cerveaux. Ce post sera utile pour les développeurs Web qui pourront être inspirés pour accélérer le rendu de leurs propres pages, et d’un intérêt tout particulier pour ceux qui connaissent déjà cette page.

Charger ses CSS/JS

Toute la difficulté du CSS, c’est que pendant qu’il est téléchargé, le rendu est bloqué. On pourrait alors tout mettre inline (comme la home de Yahoo!), mais c’est alors sacrifier les bénéfices du caching sur les fichiers statiques externes. Et le cache sur un cybermarchand est extrêmement utile puisqu’on peut s’attendre à ce qu’un utilisateur visite plusieurs pages d’affilé.

Tout est donc question de balance entre l’inline et l’external, voici plusieurs stratégies :

CSS différé

L’idée est de mettre le minimum de styles inline : un reset CSS, un peu du header et un préchargement d’images, et charger en JavaScript le CSS externe. Malgré un start render au plus bas (200ms!), ceci déclenche malheureusement un FOUC désagréable le temps que le CSS final arrive.

[caption id=”attachment_653” align=”alignleft” width=”150” caption=”Contenu non stylé pendant 1s”]Screenshot Flash of Unstyled Content[/caption]

[caption id=”attachment_654” align=”alignright” width=”150” caption=”Après 1s le contenu s'affiche”][/caption]

Ces 2 screenshots sont extraits de cette série. Vous pouvez tester cette technique sur vos navigateurs avec Cuzillion. Mais par principe un site ne devrait pas dépendre de JavaScript au point de ne pas être stylé si le JS n’est pas exécuté

Cette technique un peu extrême trouverait cependant un intérêt si le CSS était parfaitement maîtrisé, auquel cas on pourrait mettre le CSS utile et visible inline lors de la première visite (bénéfice du start render rapide), charger le CSS via JS, puis marquer l’utilisateur d’un cookie signalant qu’il a bien chargé la feuille de style pour ne lui servir que l’URL du fichier (bénéfice du cache).

CSS inline

Une autre stratégie pour ne pas avoir à télécharger un CSS qui bloque le rendu est de mettre son contenu inline. Pour cette page en particulier, certains ont réussi à réduire le poids du CSS à 4Ko gzipé, alors que le HTML fait 8Ko gzipé. La taille totale est donc largement acceptable et cela épargne un blocage de 100ms, mais c’est le genre de technique qu’on n’utilise que sur des pages très surveillées et généralement uniques (les homes de Yahoo! et de google par exemple).

Mix Inline / Inclusion

Une technique que je voulais voir en action et qui va au contraire des règles établies : mettre le minium inline et charger plus loin dans la source le reste des CSS. Ceci permet en théorie de différer le téléchargement et le parsing du CSS et de lancer au plus vite le téléchargement des images qui se trouveraient avant l’inclusion. Le tout sans dépendre de JavaScript.

Mais si IE commence effectivement le téléchargement des images, il attend tout de même le CSS avant de commencer le rendu (voir l’effet simplifié sur Cuzillion ou l’effet sur la page finale, créée par Timothée Carry-Caignon). C’est à confirmer mais le seul cas où cela pourrait marcher serait sur une page longue à calculer server-side, qui enverrait des bouts de HTML qui ne contiennent pas encore d’appel CSS. Mais même là je pense que le progressive rendering ne serait pas satisfaisant car il y aurait un effet de freeze au moment où le navigateur découvrirait qu’il y a un CSS à télécharger, laissant l’utilisateur bloqué sur une page pas encore stylée.

Un autre essai a été tenté ici par Thomas ROULET : inclusion inline d’un CSS s’occupant du layout pour que le premier rendu soit satisfaisant. Sans JavaScript, il utilise la balise noscript pour télécharger le CSS. Voici le rendu :

[caption id=”attachment_672” align=”alignleft” width=”270” caption=”Rendu avec le CSS inline”][/caption]

[caption id=”attachment_673” align=”alignright” width=”270” caption=”Rendu avec le CSS final”][/caption]

Cette technique cumule les avantages, mais pour d’autres raison le temps pendant lequel la page restait avec le CSS minimal était énorme

Conclusion : ne changez rien, il faut faire télécharger le CSS au plus tôt. Si votre CSS est vraiment énorme, outre un refactoring, envisagez le découpage en 1 feuille légère et spécifique à une page que vous placerez inline et à pré-charger les CSS des pages suivantes.

Javascript différé

Certains ont utilisé des lazy-loaders, d’autres ont utilisé l’attribut defer (inventé par IE, standardisé dans HTML5, supporté par tous) voir async. Voici un petit graphe qui résume les effets de ces attributs, tiré de cet article :

[caption id=”attachment_666” align=”aligncenter” width=”620” caption=”Comportement du navigateur avec async et defer”][/caption]

Dans les 2 cas cela demandait des modifications de code qui n’ont pas été très lourdes et qui apportent un réel plus à la fluidité du chargement de la page.

Lazy-loading de JS

Outre le chargement différé, il était aussi possible de ne charger le JS qui régit l’autocomplete du champs de recherche qu’au focus sur ce champs. C’est la technique dite du lazy-loading qui permet de ne charger les dépendances d’une fonctionnalité que lorsqu’on va en avoir besoin. Voir par exemple la home de Yahoo! qui est utilise à fond ce principe.

Paralléliser

En performances on commence toujours par là où ça va le moins vite, et là on arrive forcément à IE6/7. Ce navigateur n’autorise que 2 téléchargement de JS/CSS en parallèle. La technique généralement pratiquée est de toute façon de n’avoir qu’un seul fichier de chaque type à télécharger, mais Cédric Morin a testé le regroupement puis le split des JS/CSS en 2 fichiers pour maximiser l’utilisation de la bande passante. Résultat sur le JS :

[caption id=”attachment_670” align=”aligncenter” width=”396” caption=”Téléchargement parallèle des 2 seuls JS de la page”][/caption]

Ce sont les barres bleues (le téléchargement) qui nous montrent le gain. On doit être ici à 70ms, ce qui n’est pas énorme mais sur un site avec un JS plus lourd cela peut commencer à être intéressant. Et dans le cadre d’un concours de peformance, l’intérêt est évident :) Par contre, il faut bien faire attention à l’ordre d’exécution des scripts, mais cela peut se ranger dans des fonctions simples ou vous pouvez utiliser des librairies.

Concernant le CSS par contre, la parallélisation n’amène rien en terme d’accélération du rendu car les temps de téléchargement s’additionnent (à tester sur IE7 avec Cuzillion), mieux vaut donc rester sur un fichier unique.

Techniques de lazy-loading

Une des grandes techniques de la performance Web est de maîtriser parfaitement le chargement des dépendances, en les chargeant si possible au tout dernier moment, juste avant que l’utilisateur n’en ait besoin. C’est en pratique difficilement applicable les yeux fermés en production car il n’y a rien de générique, hormis quelques librairies qui chargent les images non visibles au moment où l’utilisateur scroll. Comme souvent dans les perfs, on est ici dans du spécifique pour chaque page et c’est au développeur de bien la connaître et de savoir ce qui est important à télécharger durant la première seconde et ce qui peut être remis à plus tard.

Cette technique semble idéale si l’on ne regarde que les outils de mesure automatisés puisqu’il y a beaucoup moins d’objets à télécharger (les outils ne scrollent pas) : le poids initial est réduit et le temps onload raccourci. Ca a donc remonté des concurrents en haut du tableau. Mais dans le cas de la FNAC, il était par exemple non toléré de :

  • différer le chargement des images produit : dans un magasin, le visuel et le prix sont 2 informations de première importance. Pas de photo ou de prix, pas d'achat. Même si la population sans javascript est de quelques %, il faut au moins que ces gens puissent voir le produit. Sur Internet, les cybermarchands espèrent en plus que des moteurs comme Google Image indexeront leurs images. Notez que pour un autre business qui accepterait de sacrifier ses utilisateurs sans JS et l'indexation (un réseau social par exemple), cette technique très efficace aurait été acceptée
  • déclencher des téléchargements au onmousemove : l'accessibilité au clavier en prend un coup

Les concurrents de la catégorie meilleur poids ont choisi diverses techniques de chargement différé :

  • placer dans une balise <noscript> le contenu de la boutique référençant les images. C'est bien vu car les gens sans JS et le référencement ne sont pas gênés. Le contenu est ensuite récupéré en JS et les images téléchargées en fonction de la zone visible. Cela a été fait avec plus ou moins de bonheur par les finalistes, mais regardez cette page du mythique Cédric Morin avec JavaScript désactivé pour avoir la meilleure version
  • la fnac ayant déjà fait le choix original de mettre son footer en iframe, les concurrents ont simplement retardé ou conditionné son affichage en JS, certains utilisant <noscript> pour la version sans JS.

Encodage des images

La réduction du nombre de requêtes HTTP étant le nerf de la guerre, cela peut aller jusqu’aux images. La technique répandue aujourd’hui est l’utilisation de sprites. Il est fortement probable qu’elle soit remplacée demain par la technique d’encodage des images dans le CSS.

MHTML et data:uri

Internet Explorer 6 et 7 supportent l’encodage des images en MHTML tandis que les autres navigateurs supportent l’encodage en base64. Voici un exemple de CSS fait par Christophe Laffont ne contenant que des images encodées et son équivalent pour les autres navigateurs. Cette technique est largement industrialisable du moment que l’on maîtrise la fabrication de son CSS final avant mise en production. Plusieurs outils ont été utilisés : il existe même une librairie PHP qui s’occupe de concaténer les feuilles de style et de séparer les règles sans images des feuilles contenant les images encodées. Au niveau des outils :

Nous attendions beaucoup du MTHML, et ce déploiement en situation réelle a révélé une énorme lacune. Ce graphique d’une vue de la page en cache est révélateur :

[caption id=”” align=”aligncenter” width=”558” caption=”Le coût CPU du MHTML”]Page load waterfall diagram[/caption]

Le HTML et toutes les dépendances viennent du cache, aucun téléchargement sauf le tracking n’est nécessaire … mais la barre des téléchargements montre un énorme vide, expliqué par le graphe CPU : il faut une pleine seconde avant de rendre utilement la page !

Les extrêmes

Certains concurrents ont tenté des techniques extrêmes qui ont été révélatrices. Parfois disqualifiantes mais très instructives

1 seule requête HTTP

Une seule requête HTTP au lieu des dizaines à l’origine, c’était théoriquement possible, et Arnaud Lemercier l’a osé. La page s’affiche bien et est fonctionnelle sous Firefox, mais ne marche pas sous IE7, ce qui était disqualifiant, mais ça nous apprend plusieurs choses :

[caption id=”attachment_618” align=”aligncenter” width=”610” caption=”1 query to rule them all”][/caption]

  • Le poids total de la page ainsi arrangée était de 500Ko. Et pourtant regardez l'utilisation de la bande passante (graphique vert) : le navigateur fait des pauses pendant le téléchargement, mettant finalement 7s au lieu de moins d'une seconde théorique
  • le processeur est occupé à 100% pendant 6 secondes : sans doute un mix de décompression Gzip sur un fichier qui peine à arriver, de la lecture d'un DOM extrêmement lourd et d'interprétation des images en base 64
  • les premiers pixels ne sont affichés qu'au bout de 4,4 secondes, et on observe le même phénomène, moins gravement, sur Firefox

Je cite les autres désavantages de la méthode inline :

  • pas de mise en cache : les visiteurs allant sur une seconde page devront re-télécharger la plupart des éléments, ce qui est dommage pour un site comme la FNAC, mais qui n'est pas grave pour certains sites "de destination" comme la homepage de Yahoo!
  • la technique des data:uri est incompatible avec IE6-7, il faut donc rajouter une technique équivalente appellée MHTML. Et donc doubler le poids des images encodées ou faire de la détection de navigateur

En résumé : mettre inline CSS, JS et surtout images encodées en base64 est extrêmement consommateur de ressources, quel que soit le navigateur au point d’empirer la situation du start render. Il serait intéressant de savoir si c’est la construction d’un DOM fait de 500k de texte ou si c’est le décodage des images qui plombe à ce point les performances de rendu. Si quelqu’un a du temps pour nous tester cela, le code source de cette page extrême est ici : http://entries.webperf-contest.com/4cd9d06f380f8/