journal

Optimiser le front end (1) : les performances du chemin critique du rendu

Cet article amorce une série d’articles visant à éclaircir et lister des ressources, des outils et des bonnes pratiques pour l’optimisation du développement front end. Ce premier billet aborde une des pierres angulaires de cette optimisation : la question de l’amélioration des performances et de la vitesse d’une page web ou d’une application.

Optimiser les performances du chargement d’un site web est crucial autant d’un point de vue ergonomique afin améliorer l’expérience des utilisateurs que d’un point de vue commercial pour convertir ces derniers. Pour autant, la notion de “chargement” ou de “vitesse de chargement” reste assez vague et ne nous apporte pas forcément des critères pertinents pour évaluer ce qui est optimisé. Avec cet article je vais essayer de comprendre quels sont les critères essentiels à prendre en compte pour évaluer les performances d’un site web, et quelles sont les moyens pour le développeur front end de les améliorer.

Comprendre le critical rendering path

Pour cet article je vais partir d’un postulat : celui qu’une meilleure expérience utilisateur dépend de la vitesse d’un site web à s’afficher. Ou du moins l’“illusion” que celui-ci est chargé puisque l’important reste d’afficher en priorité ce que l’utilisateur perçoit directement. Peu importe pour le moment que ce qui n’est pas visible ne soit pas encore entièrement chargé.

mental context switch

Figure 1 : au bout d’une seconde, la moyenne des utilisateurs exprime un “mental context switch”, c’est-à-dire qu’ils commencent à perdre l’intérêt qu’ils ont d’obtenir ce qu’ils sont venus chercher sur le site. Au-delà des 10 secondes, les utilisateurs quittent le site. (source : lean websites)

Pour répondre à ce postulat, il faut alors revenir à une notion de base du fonctionnement de tout site web : le chemin critique du rendu. Ce “critical rendering path” (CRP) est une séquence de différentes étapes que parcourt le navigateur pour convertir les fichiers d’un site web (HTML, CSS, etc.) envoyés au serveur en pixels s’affichant concrètement sur l’écran. Ce chemin est une notion clef des performances web puisque le comprendre et l’optimiser permettra justement d’améliorer, si ce n’est le temps de chargement total de la page, au moins la perception qu’aura l’utilisateur de ce temps de chargement.

En résumé, le chemin critique du rendu suit les étapes suivantes :

Comprendre le chemin critique du rendu nous permet déjà d’identifier les contraintes et éléments qui peuvent bloquer le rendu de la page, notamment :

le render tree

Figure 2 : le render tree se compose du DOM et du CSSOM (source)

Sur ces différentes questions, Ilya Grigorik, ingénieur spécialisé dans les performances web à Google, propose un cours en ligne (voir les liens en fin de section) qui explique clairement les différents processus du chemin critique du rendu. Ce cours prend l’exemple d’une page HTML simple et en illustre le CRP ainsi :

exemple de crp

Figure 3 : CRP d’une page HTML simple. Cet exemple simplifie le processus en partant du principe que le navigateur ne fournit pas de CSS par défaut, ce qui n’est jamais le cas.

Ici le critical rendering path est assez simple : le navigateur fait la requête de la page HTML et ne fait rien tant qu’il ne l’a pas reçu (on voit ici qu’il reste idle). Une fois le fichier reçu, il construit le DOM et affiche directement la page, puisque celle-ci ne contient ni CSS (donc pas de phase de construction du CSSOM ni de phase de layout dans cet exemple) ni JS (donc pas de phase d’exécution du script et d’éventuelles nouvelles manipulation du DOM et du CSSOM).

Un autre exemple est ensuite donné, celui d’une page ayant justement des fichiers CSS et JS externes :

exemple de crp avec des fichiers

Figure 4 : CRP d’une page HTML faisant appel à des fichiers CSS et JS externes

Dans ce second exemple, l’initialisation du CRP reste le même : le navigateur requête le HTML et attend. À la réception et à l’analyse du HTML, il requête alors le CSS et le JS, deux actions qu’il peut faire en parallèle. L’attente de la réponse du fichier CSS bloque le rendu de la page, puisque celle-ci nécessite la construction du CSSOM. Le fichier JS quant à lui ne peut pas être analysé par le navigateur tant que le CSSOM n’est pas construit. À la réception des fichiers, le navigateur construit donc le CSSOM, puis exécute le javascript, finit de construire le DOM éventuellement manipulé par le javascript, et affiche enfin la page.

Ces détails sur le chemin critique du rendu nous permettent de comprendre les trois critères essentiels à prendre en compte pour les performances de chargement d’un site web, et donc les trois objectifs que nous devrions suivre pour les optimiser :

Ainsi, si l’on applique ces trois critères à la figure 3 par exemple, on calcule qu’il y une ressource critique (le HTML), que celle-ci fait 5 kB et qu’il ne faut qu’une boucle réseau pour rendre la page (requête et réponse du HTML). La figure 4 est légèrement moins performante en revanche : 3 ressources critiques (le HTML, le CSS, le JS), 11 kB de ressources critiques et 2 boucles réseaux (requête et réponse du HTML, requête et réponse du CSS et du JS faits en parallèle). On imagine dès lors la complexité d’un CRP nécessitant plusieurs ressources CSS, JS, fontes, etc.

Premier objectif : Limiter les ressources critiques

comparaison de crp

Figure 5 : différence d’affichage entre un site dont le chemin critique du rendu est optimisé et un site où il ne l’est pas (source)

Les outils de critical CSS pour distinguer le style critique

Afin d’afficher la page, le navigateur a besoin de construire le “render tree” sur la base du DOM et du CSSOM. Il attend ainsi d’avoir le CSS nécessaire à la page avant d’afficher celle-ci. Pour autant, le navigateur n’a pas forcément besoin de connaître tout le CSS de mon application ou de mon site web pour afficher la première page. Même plus précisément, il n’a besoin de connaître que le style de la portion de page que verra l’utilisateur au chargement, c’est-à-dire la section au-dessus de la ligne de flottaison (si l’on met de côté le débat sur l’existence de cette dernière). L’idée est donc de proposer deux CSS différents, au minimum :

La difficulté tient alors dans le fait de déterminer ce qui relève du CSS critique ou non. Certains le font directement à la main, ce qui peut s’avérer rapidement complexe. Pour les autres, des outils assez efficaces existent, et sont notamment listés par Ben Edwards sur CSS tricks. Pour ma part j’utilise la contrib Grunt criticalcss qui génère le CSS critique à partir d’un fichier donné et de quelques informations (taille de fenêtre ciblée, buffer, etc.).

L’attribut async pour signaler les scripts non critiques

Si tous les scripts JavaScript sont bloquants pour l’analyseur, c’est parce que le navigateur ne sait pas si le script agira sur le DOM et le CSSOM tant qu’il ne l’a pas parcouru. Dans le doute il bloque donc le rendu pour analyser et interpréter le CSS. Une bonne pratique dans ce cas est d’aider le navigateur en indiquant qu’un script donné n’est pas critique et qu’il ne doit pas forcément être exécuté de manière synchrone. En ajoutant l’attribut async à la balise du script, le navigateur fait la requête du fichier au serveur, mais il continue de construire le DOM. Le script sera exécuté lorsqu’il sera reçu, mais son exécution n’a pas bloqué la construction du DOM et le rendu.

exemple de fichier async

Faire du chargement à la demande des images

L’idée est toute simple et symbolise cette optimisation du Critical Rendering Path : pourquoi faire bloquer l’affichage avec des requêtes pour des contenus qui ne sont pas directement visibles pour l’utilisateur ? Pourquoi ne pas faire ces requêtes au moment même où l’utilisateur en a réellement besoin ? C’est une optimisation radicale et essentielle, notamment en ce qui concerne l’affichage des images : nous n’allons pas charger les images qui ne sont pas encore visibles.

La technique est donc d’afficher “en dur” les images du CRP afin qu’elles soient affichées rapidement, et pour les images qui ne sont pas directement visibles, nous allons retarder leur affichage. Plusieurs plugins nous simplifient la tâche pour faire cette technique de “lazy loading” que nous recherchons : https://github.com/tuupola/jquery_lazyload est un des scripts les plus utilisés, ou encore https://github.com/vvo/lazyload, utilisé par des grands groupes comme lequipe.fr, le monde.fr ou voyages-sncf.com.

Second objectif : Minimiser la taille des ressources

Optimiser les images

En moyenne aujourd’hui, une page web fait 1,5 mB et les principaux responsables de ce poids sont les images (60% du poids d’un site en moyenne). La priorité reste donc d’optimiser les images afin de réduire leur poids au maximum sans pour autant trop altérer leur qualité. Pour cela les task runners comme Gulp ou Grunt sont très pratiques, et proposent notamment gulp-imagemin et grunt-contrib-imagemin.

Au-delà du simple fait de compresser et d’optimiser les images, il est également intéressant de s’interroger sur les formats les plus optimaux en fonction de l’image elle-même, voire parfois de remplacer une image par des formes vectorielles (svg), des web fontes ou encore des effets CSS. Google developper propose ainsi une revue des questions à se poser pour optimiser des images.

“Minifier” le CSS et le javascript

Les espaces et l’aération des fichiers sont utilisés pour la lisibilité et la maintenabilité des fichiers pour les développeurs, pas pour les machines. Le navigateur ne s’intéressant pas aux espaces, il est judicieux de les retirer des fichiers de production afin de minimiser leur poids. Plusieurs tâches Grunt et Gulp permettent ainsi de supprimer ces espaces inutiles, autant pour les fichiers CSS et JS que pour le HTML. J’utilise pour ma part grunt-contrib-uglify pour le JS et grunt-csso pour le CSS.

Activer la compression gzip

gzip est un algorithme de compression très puissant utilisé par la plupart des serveurs et supporté par tous les navigateurs modernes. Le seul soucis est qu’il n’est généralement pas activé par défaut. La solution est très simple et très bénéfique pour optimiser nos applications et site web : pour les serveurs Apache, il suffit seulement de l’activer depuis un fichier .htaccess.

example de script de compression

Prévenir la complexité des ressources

Le poids d’un fichier, s’il est primordial, n’est pas le seul enjeu à prendre en compte pour optimiser le traitement des ressources. La complexité de ces ressources a également sa propre influence : plus une ressource est complexe, plus le navigateur nécessitera du temps pour l’interpréter.

Au niveau de la structure de nos pages, une première bonne pratique à avoir est de s’appliquer à écrire du HTML aussi sémantique et valide que possible. Plus il y a d’erreurs dans le HTML, plus le navigateur devra fournir du travail (et donc du temps) pour rectifier ces erreurs pour construire le DOM.

Au niveau du style de nos pages, un autre exemple simple est donné par la notion de profondeur d’un sélecteur CSS. Plus le sélecteur CSS sera complexe et dépendant de plusieurs niveaux de noeud (fils de… fils de… etc.), plus le navigateur devra calculer et prendre en compte les contraintes pour trouver le ou les éléments sélectionnés.

exempel d'amélioration du css

Figure 6 : une classe seule (à gauche) permet d’identifier plus rapidement des éléments et demande moins de calcul au navigateur qu’un sélecteur complexe présentant plusieurs niveaux de profondeur et des expressions spécifiques.

La manière dont le navigateur interprète le CSS n’est pas intuitive. Il le parcourt en effet de la droite vers la gauche. Aussi, sur le deuxième exemple de la figure 6, le navigateur va parcourir la page pour trouver toutes les balises input, puis va parcourir les parents de ces inputs afin d’essayer de trouver un .form-group, et ainsi de suite. L’idéal est ainsi d’utiliser des sélecteurs les plus spécifiques possibles, et de minimiser au possible les imbrications inutiles dans le CSS, souvent malheureusement favorisées par les “nested selectors” des prép-processeurs. Il est ainsi recommandé de se limiter à 3 niveaux de sélecteurs (c’est ce que recommande les designers d’AirBNB par exemple), voire à 4 (ce que recommande l’ “inception rule”) ce qui, dans une perspective d’approche modulaire du code, ne devrait pas être trop difficle.

Un outil comme grunt-css-count fournit par ailleurs des indicateurs intéressants sur le CSS et permet d’avoir un audit automatisé de l’utilisation des différents types de sélecteurs.

rendu du css count

Figure 7 : csscount renvoie des informations comme le nombre de sélecteurs utilisés, le poids d’un fichier ou encore la répartition des sélecteurs selon leur niveau de profondeur dans le CSS (de D1 à D11). À noter que connaître le nombre de sélecteurs est essentiel dans certains projets web puisque IE9 est limité à 4095 sélecteurs

De manière générale enfin, c’est sans doute la complexité globale de l’application ou du site web qu’il est généralement pertinent d’interroger. Certains éléments ne sont pas forcément nécessaires ni pour véhiculer un message ni pour l’expérience générale sur le projet. Comme le rappelle Ilya Grigorik, la ressource la plus rapide et la plus optimisée reste celle qui n’est pas envoyée. Par exemple, il peut être pertinent de questionner l’intérêt d’un carousel : les utilisateurs le consultent-ils, même partiellement ou en totalité ? Son intérêt est-il pertinent au regard de ce qu’il “coûte” en terme de performance ? Le design d’un site web et ses performances sont ainsi liés : si une bonne expérience utilisateur commence par un site fluide et rapide, un design soigné et pertinent permet à l’inverse d’éliminer dès la conception des obstacles non négligeables à la rapidité et à la réactivité du site.

Troisième objectif : Optimiser les requêtes

Concaténer les ressources

Le meilleur moyen d’optimiser les requêtes reste tout d’abord d’en réduire au maximum le nombre. Les connexions HTTP pouvant prendre un temps conséquent, notamment sur mobile ou le temps de latence est souvent plus long, en limiter le nombre permettra d’optimiser grandement le temps de chargement de la page. Le nombre de requêtes peut être ainsi limité en concaténant, c’est-à-dire en joignant dans un même fichier, différentes ressources :

Activer la mise en cache

La plupart des recommandations de performances concerne la première connexion des visiteurs sur un site ou une application. La mise en cache permet ensuite d’optimiser ces ressources pour les utilisateurs qui reviennent sur le site. En effet, un utilisateur qui revient sur le site ne devrait pas à avoir à recharger à nouveaux toutes les ressources, quand bien même celles-ci sont optimisées. C’est justement ce à quoi répond le cache HTTP en proposant de conserver ces ressources pour un temps donné dans le navigateur.

Sur les serveurs Apache, la mise en cache peut s’activer et se gérer depuis le fichier .htaccess :

fichier htaccess

Notons également que la mise en cache peut-être bénéfique sans pour autant être activée. Par exemple avec le “cross-site caching”, c’est-à-dire lorsque l’on utilise des ressources populaires, sûrement utilisées sur d’autres sites, et donc probablement déjà dans le cache du navigateur de l’utilisateur. C’est par exemple le cas lorsque l’on utilise des fontes populaires chargées depuis Google Fonts (et non en local) ou encore lorsque l’on utilise des CDN pour les librairies javascript les plus utilisées. Il est ainsi probable que la plupart des utilisateurs aient déjà la fonte Open Sans et la librairie jQuery dans le cache de leur navigateur.

Préparer les requêtes pour le navigateur

Les navigateurs modernes sont optimisés pour tenter de prédire et d’anticiper l’activité des utilisateurs sur un site web. Or, plusieurs outils existent afin de justement aider le navigateur dans ces prédictions, et de lui indiquer et préparer les requêtes à venir afin d’accélérer leur prise en compte. Après tout, nous connaissons mieux le comportement plausible des utilisateurs ainsi que les ressources qui pourraient être nécessaires à court terme.

Parmi ces outils, on trouve ainsi :

example de prefetching

exemple de subresource

exemple de prefetching

exemple de prerendering

Limiter les modifications du DOM

Cette dernière recommandation concerne peut-être moins la question de l’optimisation des requêtes mais reste néanmoins pertinente pour l’amélioration globale du chargement de la page et pour la réduction du chemin critique du rendu. Comme nous avons pu le voir, la construction du DOM est assez lente. Solliciter des nouvelles constructions peut demander inutilement beaucoup de temps, surtout qu’une modification sur le DOM obligera le navigateur à recalculer le style des éléments et donc sa mise en page. Il en va de même pour la manipulation du CSSOM. On y réfléchira ainsi à deux fois à la réelle utilité de modifier les éléments d’une page à l’aide de jQuery par exemple.

Par ailleurs, la question des performances devient un critère crucial pesant en faveur d’un framework qui limite les modifications directes ou complètes du DOM, à l’image de React par exemple qui utilise l’intermédiaire d’un DOM virtuel optimisé pour les modifications.

Mesurer pour évaluer

Les recommandations ci-dessus ne représentent qu’une partie de ce qu’il est possible de faire pour améliorer les performances de son site ou de son application. On trouvera dans les ressources plus bas des articles proposant d’autres méthodes pour approfondir cette démarche. Un dernier point à aborder succinctement reste néanmoins crucial : afin d’évaluer pertinemment les performances d’une application, il faut pouvoir mesurer ces performances. Voici donc une liste d’outils pertinents pour ces mesures :

chrome devtool network

Figure 8 : Aperçu de l’outil “network” de Google Chrome au chargement de la page d’accueil du monde.fr. On y voit le “resource waterfall” qui liste toutes les requêtes envoyées par la page afin qu’elle s’affiche. En bas à gauche nous pouvons retrouver des critères qui nous intéressent particulièrement dans l’optique d’une optimisation : le nombre de requêtes nécessaires à l’affichage (440 ici), le poids total de la page (1,5 mB) et le temps de chargement complet (21 secondes).

chrome devtool network

Figure 9 : Analyser les temps de chargement et de rendu de chaque fichier nous permet ainsi d’identifier les sources à optimiser. Dans cet exemple donné par Barbara Bermes on constate que plusieurs gros fichiers CSS sont chargés, que les images mettent du temps à être récupérées et encore plus de temps à s’afficher ou encore que plusieurs requêtes javascript sont envoyées.

chrome devtool timeline

Figure 10 : Aperçu de l’outil “timeline” de Google Chrome au chargement de la page d’accueil du monde.fr. La timeline permet par ailleurs d’identifier la répartition du temps en fonction des différentes actions du navigateur, et permet donc d’identifier les éventuelles actions prenant trop de temps.

chrome devtool audits

Figure 11 : Aperçu de l’outil “audits” de Google Chrome au chargement de la page d’accueil du monde.fr. Cet onglet analyse la page lors de son chargement et propose des suggestions sur l’optimisation de ses performances.

overview pagespeed

overview pagespeed

Figure 12 : Analyse PageSpeed du site lemonde.fr. On y voit le score pageSpeed ainsi que les recommandations à suivre afin d’améliorer ce dernier.

webpagetest

Figure 13 : Aperçu de WepPageTest au chargement de la page d’accueil du monde.fr. On voit ainsi que l’outil est beaucoup plus précis, et propose des critères plus détaillés comme le “first byte” (temps entre le début de la connexion et la réception du premier byte par le navigateur, après les éventuelles redirections ou les éventuelles latences) ou encore le “start render” (temps avant que le contenu commence à apparaître). Un critère très utilisé est aussi celui du speed Index, le temps moyen (en ms) auquel les parties visibles (donc critiques) d’une page sont affichées. Paul Irish recommande qu’il soit inférieur à 1000.

webpagetest

webpagetest

Figure 14 : WepPagetest permet également de faire des comparaisons entre plusieurs sites, ici par exemple entre lemonde.fr et lequipe.fr. Les résultats affiches des aperçus du chargements ou encore plusieurs graphs illustrant les différences de performances entre les sites comparés (speed index, tailles des ressources, requêtes, …). Pertinent pour comparer son site à celui de ses concurrents, ou encore pour comparer un site optimisé en preprod avec son ancienne version non-optimisée.

illustration de l'outil big rid

Figure 15 : Aperçu d’un graphe issue d’une trace, généré par Big Rid (source)

Les performances de chargement d’un site web sont un enjeu crucial pour tout projet web, car elles sont le fondement de dimensions aussi essentielles et différentes que l’expérience utilisateur, l’image de la marque ou, comme nous le verrons dans les prochains articles, le SEO et la “soutenabilité” (éco-conception).

À ce titre, ces quelques recommandations pour optimiser le front-end pourront paraître nécessaires mais pas suffisantes. D’une part, la sensibilité du sujet fait que de nouvelles recommandations et de nouveaux outils émergent très régulièrement, rendant cette liste d’ores-et-déjà si ce n’est obsolète au moins non-exhaustive : Google vient par exemple le mois dernier de proposer un nouvel outil pour optimiser le chargement des pages sur mobile, tandis que le protocole HTTP/2 vient, en 2016, changer la donne des bonnes pratiques des WebPerfs, soulignant l’utilité de certaines méthodes (compression, cache, async, pre-browsing, …) et rendant obsolètes d’autres (concaténation des fichiers, lazyloading, …). D’autre part, des performances optimisées se pensent en réalité dès la conception et le web design, doivent également être considérés dans le choix des outils et des méthodes pour le back end, et ne peuvent donc se résumer aux seules méthodes applicables en front end.

TL;DR

Ressources sur l’optimisation des performances :