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_facebook
Figure 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 individuelles HTML (commun) CSS pour le sprite set
div.sprite-icons {
 background-repeat: no-repeat;
}
div.sprite-icons-pause {
 background-image:
       url('icon-pause.png');
 width: 60px;
 height: 60px;
}
...
<div class="sprite-icons
            sprite-icons-pause">
</div>
<div class="sprite-icons
            sprite-icons-pause-disabled">
</div>
<div class="sprite-icons
            sprite-icons-play">
</div>
div.sprite-icons {
 background-image: url('icons.png');
 background-repeat: no-repeat;
}
div.sprite-icons-pause {
 background-position: 0 -60px;
 width: 60px;
 height: 60px;
}
...

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.

Nom Images Nombre d’images
Single sprite set 100% (72)
AllSplitted individuelle 100%
80pSplitted individuelle 80%
50pSplitted individuelle 50%
30pSplitted individuelle 30%
10pSplitted individuelle 10%

 

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 :

configuration description latence vitesse d’upload
#1 VM dans un datacenter 10ms 80Mb/s
#2 PC avec une bonne connection internet 40ms 20Mb/s
#3 PC avec une connection internet dégradée 35ms 1.3Mb/s

 

Résultats des tests

L’ensemble des 3 configurations de tests fournit des résultats cohérents, affichés dans la figure 5sprites-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/

6 commentaires sur “Ne jetez pas les sprites avec HTTP/2”

  • Merci pour cet article très détaillé, avec une telle rigueur on est pas loin d'une publication scientifique. :)
  • Article intéressant, merci!. Cependant il ne s'attache qu'au premier affichage de la page. J'aurais bien aimé que cette partie de l'article ait été d'avantage développée:"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é" car c'est à mon sens un élément très intéressant de HTTP/2. Par ailleurs il me semblait que finalement HTTP/2 soit réservé au https. (#lazyweb)
  • Merci ! Pour le côté scientifique ... disons qu'on ne renie pas ses premiers amours en un claquement de doigt. Pour le rechargement des images sans invalidation du cache, de ce que j'ai pu voir il y a très peu de différences entre avec et sans sprites (et on rentre là dans la nature stochastique du réseau qui bruite très/trop fortement la mesure). De toute façon le coeur de l'article n'est pas de dire que HTTP/2 n'est pas valable, mais que cette amélioration du protocole n'est pas suffisante pour remettre en cause l'utilisation des sprites. Après bien sûr que HTTP/2 apporte un mieux par rapport à HTTP/1.x. Et peut-être bien que c'est particulièrement le cas pour les petites requêtes qui renvoie un body vide comme le code 304, mais là je n'ai pas de mesure. Pour le fait que HTTP/2 soit forcément avec TLS, c'est exact et c'est corrigé dans l'article maintenant.
  • Etude intéressante mais le benchmark possède quelques problèmes qui faussent un peu les résultats: - la compression des images indépendantes n'est pas idéale (je vous ai fait une pull request avec des images recompressées), la différence d'octets entre les 2 versions optimisées est beaucoup plus faible - la méthode de mesure n'est pas bonne car, dans le cas sprite, le load de l'image se déclenche sur le chargement de l'image transparente pas l'affichage du sprite. Il vaut mieux utiliser l'API window.performance.timing et/ou window.performance.getEntries pour avoir des mesures fiables - les mises en page ne sont pas tout à fait équivalentes entre les différents tests: dans le cas sprite, les tailles des différents blocs sont spécifiés en CSS et donc il n'y a qu'une seule étape de layout pour le navigateur; dans les autres pages, les dimensions des images ne sont pas précisées à l'avance et donc il doit faire un recalcul de layout à chaque réception d'image. Au final, il y a des chances que le sprite reste la méthode la plus rapide sur ce type de benchmark mais la différence est probablement moins marquée que celle que vous faites apparaître. Si j'ai le temps de reproduire le bench complet dans les prochains jours, je vous ferai une pull request avec les modifications ci-dessus.
  • Merci beaucoup Raphaël pour ces commentaires et la première pull request, et volontiers pour la seconde pull request, je serais très intéressé de voir la différence (et du coup je suis d'autant plus triste de ne pas avoir assisté à votre présentation au PerfUG cet automne :o)).
  • Très bon article. Je serais curieux de voir si la concaténation des vendors JS est justifiable dans le contexte d'HTTP2
    1. Laisser un commentaire

      Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha