Ne jetez pas les sprites avec HTTP/2

Dans cet article, nous démontrons que si HTTP/2 permet des gains significatifs de temps de chargement des pages, il ne remet pas pour autant en cause la réalisation d'optimisations front-end. Aujourd'hui, nous nous concentrerons sur les sprite sets.


Avec l'arrivée en cette année 2015 du nouveau protocole HTTP/2 en remplacement du protocole HTTP/1.1 en place depuis 1997, plusieurs auteurs [1,2] prédisent la nécessaire disparition d'une partie des optimisations front-end. Parmi celles-ci nous trouvons la technique des sprites consistant à encapsuler plusieurs petites images (les sprites) dans une seule image globale (le sprite set).

En dépit de l'adoption rapide de ce nouveau standard HTTP par les serveurs et les navigateurs web [3, 4], nous n'avons pas trouvé de mesures réelles permettant de supporter cette hypothèse. En tant qu'architecte, nous nous posions alors légitiment la question de savoir s'il est nécessaire de supprimer ces sprites sets lors du passage à HTTP/2, ou bien si s'agit plus d'optimisation inutile pour les nouveaux développements. Hors comme le dit W. Edwards Deming, "in God we trust, all others bring data". Nous avons par conséquent lancé notre propre campagne de test.

La première partie de cet article est un résumé des principales différences entre HTTP/1.x et HTTP/2 qui pourraient remettre en cause l'utilisation des sprites sets. Ensuite, dans une seconde partie, nous vous présentons les résultats de nos tests.

Sprite sets

Les sprite sets font partie des optimisations front-end. Au lieu de charger de nombreuses images statiques du serveur de façon individuelle (pensez par exemple à une collection d'icônes utilisées dans tout le site web), un seul sprite set est chargé et ensuite découpé par le navigateur pour former les images individuelles. Le sprite set utilisé dans notre test, issu de www.facebook.com, est affiché en figure 1.

sprite_facebookFigure 1: le sprite set de nos tests.

Ce sprite set est composé de 72 sprites.

La première observation est que ce sprite set en tant qu'image globale utilise 71 kB, tandis que les sprites découpés en images individuelles et sauvegardées utilisent un total de 106 kB, soit une augmentation de presque 40 %. Le volume global est donc plus petit que la somme des volumes individuels, grâce à une meilleure compression de l'image et une réduction du volume des headers de fichier. En outre, une seule requête est suffisante pour charger toutes les icônes du site avec un sprite set, au lieu de multiples requêtes pour charger chaque image individuelle.

Pour afficher les images individuelles, le navigateur va découper le sprite set grâce à du code CSS développé spécifiquement en fonction du positionnement de ces images [6]. Dans la figure 2, vous pouvez voir le code HTML commun utilisé avec et sans les sprites, et les codes CSS correspondant. La chronologie de chargement des icônes est également affichée.

CSS pour les images individuellesHTML (commun)CSS pour le sprite set
div.sprite-icons {<br> background-repeat: no-repeat;<br>}<br>div.sprite-icons-pause {<br> background-image:<br> url('icon-pause.png');<br> width: 60px;<br> height: 60px;<br>}<br>...<div class="sprite-icons<br> sprite-icons-pause"><br></div><br><div class="sprite-icons<br> sprite-icons-pause-disabled"><br></div><br><div class="sprite-icons<br> sprite-icons-play"><br></div>div.sprite-icons {<br> background-image: url('icons.png');<br> background-repeat: no-repeat;<br>}<br>div.sprite-icons-pause {<br> background-position: 0 -60px;<br> width: 60px;<br> height: 60px;<br>}<br>...

sprites-timeline-withspritessprites-timeline-nosprites

Figure 2: avec et sans sprite set. Le code est affiché dans la partie supérieure. Dans la partie inférieure se trouve la chronologie des requêtes HTTP. Sans sprite set, on observe de nombreux appels concurrents au serveur, avec un temps global d'exécution plus important.

Nous pouvons observer à quel point le code CSS code est plus simple sans sprite set, mais le temps de chargement est plus important. Quelques limitations techniques limitent néanmoins l'intérêt des sprite sets et sont discutées ci-dessous.

Les principales limitations à l'utilisation des sprite sets sont:

  • un développement plus complexe puisqu'il faut créer un sprite set, et isoler les zones à afficher au lieu de simplement afficher les sprites individuels
  • une invalidation du cache du navigateur à chaque changement dans le sprite set, même s'il ne s'agit que d'un seul sprite modifié, retiré, ajouté

HTTP/1.x et les sprites

En HTTP/1.x, il n'y a qu'une seule requête en cours par connexion TCP entre le browser et le serveur. Les requêtes suivantes doivent attendre la libération de la connexion TCP pour pouvoir la réutiliser. Pour néanmoins obtenir des performances correctes et éviter de bloquer le chargement de la page par une longue requête, les browsers ouvrent plusieurs connexions TCP en parallèle avec le serveur (de 2 à 8 connexions suivant le browser [5]). Cependant, ce degré de parallélisme est relativement limité et multiplier les requêtes continue à prendre du temps, de la bande passante, et de la charge sur le back-end.

Ainsi, avec l'utilisation d'un sprite set une seule requête est effectuée entre le browser et le serveur pour le chargement de toute l'iconographie, et les performances sont alors grandement améliorées.

HTTP/2 et les sprites

Avec HTTP/2, toutes les requêtes entre le browser et le serveur peuvent être multiplexées sur une seule connexion TCP [7]. Cela permet de tirer un profit maximum de cette connexion TCP et de limiter autant que possible les effets de latence entre le client et le serveur.

Il deviendrait alors possible de charger en parallèles des dizaines d'images sur une même connexion TCP. Et par conséquent, il deviendrait inutile de recourir à l'utilisation de sprites sets pour limiter le nombre de requêtes. Toutes ces phrases sont au conditionnel car nous allons voir dans les tests que si la théorie du protocole HTTP/2 permet certaines choses, ces choses sont plus complexes qu'il n'y semblerait une fois mises en pratique.

Framework de test

Tout le code nécessaire pour reproduire ce test est disponible sur GitHub [8]

Pour reproduire diverses situations, six pages HTML ont été développées. La première tire partie de l'utilisation d'un sprite set, tandis que les autres utilisent une quantité variable des images individuelles.

NomImagesNombre d'images
Singlesprite set100% (72)
AllSplittedindividuelle100%
80pSplittedindividuelle80%
50pSplittedindividuelle50%
30pSplittedindividuelle30%
10pSplittedindividuelle10%

Les 4 dernières pages contenant seulement une portion des sprites permettent de représenter les performances du cas classique où seulement une portion des sprites sont affichées sur la page, les autres étant utilisés dans d'autres contextes en fonction de l'état de page (langue de la page, position géographique de l'utilisateur, ...). En utilisant des images individuelles, il serait donc possible de ne charger que les sprites requis par l'état actuel de la page.

Un code Javascript a été développé dans les pages pour chronométrer le temps entre le chargement de la page HTML (exécution du body des scripts JS dans le header de la page) et le dernier chargement d'image (évènement 'onload'). C'est ce temps de chargement qui sera mesuré et comparé pour les différent cas.

Coté serveur, ces pages et les images associées ont été positionnées sur deux serveurs NGINX 1.9.5 situés dans le même datacenter, l'un servant les pages en HTTP/1.1, l'autre en HTTP/2. Les pages sont requêtées en HTTPS, y compris en HTTP/1.1, pour une comparaison adéquate avec HTTP/2 qui est forcément sécurisé.

Coté client, un script Python a été développé pour requêter ces pages dans les navigateurs Firefox 41.0 et Chrome 45.0, pilotés par Selenium WebDriver [9]. Selenium permet de fournir un nouveau contexte de navigateur à chaque appel, afin de ne pas déjà avoir les images en cache dans le navigateur. En effet, si les images sont en cache dans le navigateur le temps total de requêtage devient trop faible (de l'ordre de la dizaine de millisecondes au total) pour pouvoir être mesuré précisément avec cette approche et les différences entre HTTP/1.x et HTTP/2 sont très faibles. Selenium permet enfin de récupérer le temps mesuré par le code Javascript à chaque chargement de page.

sprites_protocol

Pour s'assurer d'une représentativité des mesures, le protocole contient 100 répétitions, selon le pseudo-code ci-dessous.

for i = 1 to 100
  for page in ('Single', 'AllSplitted', '80pSplitted', '50pSplitted', '30pSplitted', '10pSplitted')
   for protocol in ('HTTP/1.1', 'HTTP/2')
     for browser in ('Firefox', 'Chrome')
       #load page and measure load time

Pour chaque cas, la médiane des 100 répétitions est suivie. En effet, si l'on regarde une distribution (cf. figure 4), on note quelques rares cas où des temps importants sont mesurés de manière relativement sporadique et sans aucun doute liés à la nature stochastique du réseau. La moyenne est alors exagérément augmentée et risque d'être bruitée d'un cas à l'autre. La médiane est un indicateur correct car en dehors de ces quelques points extrêmes, le reste de la distribution est plus proche d'une distribution homogène que d'une distribution normale.

sprites-graph1

Figure 4: temps de chargement pour 100 répétitions de la même mesure.

Le protocole a été répété sur 3 trois configurations de clients :

configurationdescriptionlatencevitesse d'upload
#1VM dans un datacenter10ms80Mb/s
#2PC avec une bonne connection internet40ms20Mb/s
#3PC avec une connection internet dégradée35ms1.3Mb/s

Résultats des tests

L'ensemble des 3 configurations de tests fournit des résultats cohérents, affichés dans la figure 5 sprites-graph2

Figure 5: temps de chargement médian pour les différentes pages, configurations, navigateurs et protocoles HTTP.

Les observations que l'on peut faire sont les suivantes :

  • le temps de chargement du sprite set est au plus équivalent au chargement de 10% des sprites dans le cas d'une connexion à très faible latence. Dans tous les autres cas du test, le sprite set est nettement plus rapide à charger que les sprites, peut importe le protocole HTTP utilisé ;
  • HTTP/2 apporte bien un gain vis à vis de HTTP/1.1. d'un point de vue global, mais cette amélioration du protocole n'est pas suffisamment significative pour remettre en cause l'utilité des optimisations front-end ;
  • le browser utilisé importe peu (les différences de temps dans la configuration 1 sont sans doute plutôt du au fait que la VM était dimensionnée un peu juste)

On peut également tracer les temps en fonction du nombre de requêtes ou du volume total. La figure 6 ci-dessous représente ce résultat pour la configuration 3 mentionnée ci-dessus.

sprites-graph3sprites-graph4

Figure 6: les résultats de la figure 5 limités à la configuration 3, en affichant le temps de chargement en fonction du nombre d'images et de leur volume.

On voit clairement que le sprite set se distingue vis-à-vis des sprites individuels non pas en raison du volume (qui est similaire au volume de 50% des sprites individuels) mais en raison de l'unicité de la requête à effectuer. On voit également très bien apparaitre ici aussi le gain entre HTTP/1.1 et HTTP/2.

Conclusion

En conclusion, cet essai fait apparaitre que le protocole HTTP/2 ne semble aucunement remettre en cause l'utilisation de sprites sets pour optimiser les temps de chargement de pages web. En effet, le protocole permet effectivement des gains de temps très significatifs (jusqu'à plus de 50% de réduction) par rapport à HTTP/1.1, mais cette amélioration reste limitée par rapport au supplément de requêtes à effectuer si l'on n'utilise pas les sprites sets. HTTP/2 permet de mieux utiliser le lien réseau disponible, mais HTTP/2 ne permettra pas pour autant de revoir les optimisations front-end actuelles, parmi lesquelles figurent les sprites, la minification des CSS et JS, et les bundles.

Références

[1] https://mattwilcox.net/web-development/http2-for-front-end-web-developers [2] http://http2-explained.haxx.se/content/en/part3.html [3] https://en.wikipedia.org/wiki/HTTP/2 [4] http://w3techs.com/technologies/details/ce-http2/all/all [5] http://stackoverflow.com/a/985704 [6] http://www.w3schools.com/css/css_image_sprites.asp [7] http://qnimate.com/what-is-multiplexing-in-http2/ [8] https://github.com/benoit74/http2-sprites/ [9] http://www.seleniumhq.org/