Beaucoup des optimisations évoquées en performance web, du CSS critique au report des scripts, de la priorité des images au préchargement des polices, n’ont de sens que si l’on comprend un processus sous-jacent : la façon dont le navigateur transforme un document HTML, quelques feuilles de style et quelques scripts en pixels qui apparaissent à l’écran. Ce processus s’appelle le chemin critique de rendu (critical rendering path), et le connaître change la façon de regarder une page lente : au lieu d’une liste de conseils à appliquer, on voit pourquoi certaines ressources font attendre l’utilisateur et d’autres non.
Dans cet article, nous le parcourons étape par étape, sans rien tenir pour acquis : ce que fait le navigateur avec le HTML, ce qu’il fait avec le CSS, où intervient JavaScript, quelles étapes mènent à l’affichage et ce qui bloque le premier rendu. À la fin, le lien avec des métriques comme le FCP et le LCP sera évident.
Du HTML au DOM
Tout commence par le document HTML. Le navigateur le reçoit sous forme d’une suite d’octets et le lit progressivement, à mesure qu’il arrive, sans attendre de l’avoir en entier. Pendant la lecture, il reconnaît les balises, les transforme en nœuds et construit une structure arborescente qui représente la page : le DOM, Document Object Model. Chaque élément, du titre au paragraphe, de l’image au bouton, devient un nœud de cet arbre, avec ses propres attributs et ses propres enfants.
Pendant la lecture, le navigateur rencontre des références à d’autres ressources : feuilles de style, scripts, images, polices. Un composant dédié, le preload scanner, parcourt le document à l’avance pour les découvrir le plus tôt possible et lancer leur téléchargement en parallèle. C’est l’une des raisons pour lesquelles une ressource déclarée directement dans le HTML est téléchargée beaucoup plus tôt qu’une ressource insérée par un autre fichier ou par un script.
Du CSS au CSSOM
En parallèle, le navigateur traite les feuilles de style et construit une seconde structure, le CSSOM, CSS Object Model, qui contient toutes les règles de style avec leurs priorités. Il y a ici une différence importante avec le HTML : le CSS ne peut pas être appliqué par morceaux. Une règle à la fin de la feuille de style peut annuler une règle du début, si bien que le navigateur doit avoir toute la feuille avant de pouvoir dire avec certitude à quoi ressemble un élément.
C’est pourquoi les feuilles de style chargées dans l’en-tête de la page bloquent le rendu : tant qu’elles ne sont pas arrivées et traitées, le navigateur n’affiche rien. C’est un choix délibéré, pour éviter de montrer la page sans styles puis de la repeindre, ce qui serait désagréable pour l’utilisateur. Mais cela signifie que chaque kilo-octet de CSS chargé de façon bloquante se trouve sur le chemin du premier rendu.
Le rôle de JavaScript
JavaScript complique le tableau, car il peut presque tout faire : lire et modifier le DOM, ajouter ou retirer des éléments, changer les styles, voire écrire du nouveau HTML dans le document. Pour cette raison, quand le navigateur rencontre un script ordinaire en lisant le HTML, il s’arrête : il doit le télécharger, s’il est externe, et l’exécuter avant de continuer, car le script pourrait modifier ce qui suit. Et comme un script peut lire les styles des éléments, le navigateur doit aussi avoir terminé les feuilles de style précédentes avant de l’exécuter.
Un script synchrone dans l’en-tête bloque donc la construction du DOM, attend le CSS et occupe le thread principal avec sa propre exécution, le tout avant que l’utilisateur voie quoi que ce soit. Les attributs defer et async changent ce comportement : avec defer, le script est téléchargé en parallèle et exécuté seulement après la lecture complète du document, dans l’ordre où il apparaît ; avec async, il est exécuté dès qu’il est téléchargé, sans ordre garanti. Les scripts de type module se comportent par défaut comme des scripts différés.
<!-- Bloque la lecture du document jusqu’à son téléchargement et son exécution -->
<script src="/js/app.js"></script>
<!-- Téléchargé en parallèle, exécuté après la lecture du document -->
<script src="/js/app.js" defer></script>
<!-- Téléchargé en parallèle, exécuté dès qu’il est prêt -->
<script src="/js/statistiche.js" async></script>
Render tree, layout, paint et composition
Quand le DOM et le CSSOM sont prêts, le navigateur les combine dans le render tree, qui ne contient que les éléments à afficher avec leurs styles calculés. Les éléments masqués avec display: none, par exemple, n’en font pas partie.
Vient ensuite le layout : le navigateur calcule la position et la taille exactes de chaque élément, d’après la largeur de l’écran, les textes, les images et les règles de mise en page. C’est un calcul qui dépend des relations entre les éléments, et son coût croît avec la taille et la complexité de la page, comme nous l’avons vu à propos du coût du DOM dans les page builders.
Puis le paint, le dessin proprement dit : le navigateur produit les pixels du texte, des couleurs, des bordures, des ombres et des images. Enfin la composition : la page peut être découpée en couches, par exemple pour les éléments animés ou en position fixe, qui sont dessinées séparément puis combinées, souvent avec l’aide de la carte graphique. Séparer les couches permet de mettre à jour une partie de la page sans repeindre tout le reste.
Ces étapes ne se produisent pas une seule fois. Chaque modification ultérieure, comme un élément ajouté par JavaScript, une image qui arrive, une police qui est substituée, peut demander de répéter le calcul des styles, le layout et la peinture, en tout ou en partie. C’est ainsi que naissent les décalages mesurés par le CLS et le travail qui ralentit les interactions mesurées par l’INP.
Ce qui retarde le premier rendu
En assemblant les pièces, le premier rendu d’une page, mesuré par le First Contentful Paint, ne peut se produire que lorsque le navigateur a reçu le HTML au moins jusqu’au contenu à afficher, a téléchargé et traité toutes les feuilles de style bloquantes et a exécuté tous les scripts synchrones qui le précèdent. Le chemin critique est l’ensemble de ces ressources et des étapes qui les relient : sa longueur détermine combien de temps l’utilisateur attend devant un écran vide.
Le chemin critique se mesure selon trois dimensions : le nombre de ressources critiques, leur poids total et le nombre d’allers-retours nécessaires pour les obtenir. Une feuille de style qui en importe une autre ajoute un aller-retour ; une police déclarée dans le CSS en ajoute encore un ; un script qui insère une feuille de style en ajoute un troisième. Ces chaînes sont bien visibles dans le waterfall des requêtes réseau des DevTools.
Le cas des polices
Les polices méritent une discussion à part, car elles occupent une position particulière sur le chemin. Le navigateur ne découvre qu’il a besoin d’une police que lorsqu’il a traité le CSS et trouvé un élément visible qui l’utilise : la police démarre donc après la feuille de style, à la fin de la chaîne. Pendant ce temps, le navigateur doit décider quoi faire du texte. Avec la règle font-display: block, il masque le texte pendant une courte période en attendant la police ; avec font-display: swap, il l’affiche tout de suite avec une police de secours et la remplace quand la police arrive. Le premier choix retarde l’apparition du texte, donc le premier rendu significatif et souvent le LCP ; le second affiche le contenu immédiatement, au prix d’un petit changement visuel que les propriétés de surcharge des métriques de police peuvent rendre presque imperceptible. Avancer la police principale avec un preload et l’héberger sur son propre domaine raccourcit encore la chaîne.
Comment raccourcir le chemin
Les techniques pour améliorer le premier rendu se comprennent mieux à la lumière du chemin critique : chacune réduit le nombre, le poids ou la profondeur des ressources critiques.
- Moins de CSS bloquant. Intégrer dans le HTML seulement les styles nécessaires à la partie visible de la page et charger le reste de façon non bloquante, comme l’explique l’article sur le CSS critique ; supprimer les règles inutilisées.
- Pas de scripts synchrones dans l’en-tête. Utiliser
deferpour les scripts de l’application,asyncpour les scripts indépendants, et repousser à l’interaction tout ce qui n’est pas nécessaire tout de suite, comme dans la technique du report du JavaScript. - Des chaînes plus courtes. Éviter
@importdans les feuilles de style, déclarer les ressources critiques directement dans le HTML, précharger les polices et les images qui seraient sinon découvertes tard. - Un HTML qui arrive tôt. Tout part du HTML : un TTFB bas, grâce au cache de page, fait avancer tout le chemin.
- Des ressources plus légères. La compression, la minification et les polices réduites aux caractères nécessaires, comme on l’a vu dans l’article sur le subsetting des polices, réduisent le temps de téléchargement de chaque ressource critique.
Du premier rendu au LCP
Le chemin critique détermine le premier rendu, mais le LCP dépend aussi d’une autre ressource : le plus grand élément de la page, souvent une image. Même si le CSS est prêt, le LCP ne peut pas se produire avant que l’image principale ait été téléchargée et décodée. Pour cette raison, l’optimisation du chemin critique doit aller de pair avec celle de l’élément du LCP : découverte précoce dans le HTML, priorité haute, format et taille adaptés. Quand c’est du texte qui est l’élément du LCP, en revanche, la police entre en jeu : si la police des titres arrive tard et que le navigateur masque le texte en attendant, le LCP se décale en conséquence.
Comment l’observer
Le panneau Performance des DevTools de Chrome montre le chemin critique à l’œuvre : lecture du HTML, calcul des styles, layout, paint, avec les ressources réseau alignées dans le temps et les moments du premier rendu et du LCP signalés sur la trace. Dans le panneau Network, les ressources bloquant le rendu se reconnaissent à leur priorité haute et à leur position au début du chargement. Lighthouse et PageSpeed Insights signalent une liste de ressources bloquant le rendu et une estimation du temps qu’on gagnerait en les éliminant ; les versions récentes montrent aussi l’arbre des dépendances réseau, avec les chaînes de requêtes critiques.
Un exercice utile consiste à comparer la pellicule d’un chargement de page avec le chemin critique : le moment où la page cesse d’être vide coïncide avec l’arrivée de la dernière ressource bloquante. Retirer cette ressource du chemin, ou l’alléger, avance visiblement la première image avec du contenu.
Un exemple
Prenons une page d’article sur un site WordPress avec un thème riche en fonctionnalités. L’en-tête contient cinq feuilles de style, dont l’une importe des polices depuis un service externe, et trois scripts synchrones, dont une bibliothèque pour les effets de défilement. Le waterfall montre une chaîne de quatre étapes avant que le navigateur puisse afficher quoi que ce soit : HTML, feuille de style principale, feuille de style importée, fichier de police. Sur mobile, le premier rendu arrive après 2,4 secondes. Les corrections sont peu nombreuses : le CSS nécessaire à la partie visible est intégré au HTML et le reste chargé de façon non bloquante, les polices sont hébergées sur le site et déclarées directement, les trois scripts reçoivent defer et la bibliothèque d’effets n’est chargée que sur les pages qui l’utilisent. Le premier rendu passe sous la seconde, sans changement visible pour le lecteur.
En bref
Pour afficher une page, le navigateur construit le DOM à partir du HTML et le CSSOM à partir des feuilles de style, les combine dans le render tree, calcule le layout, peint et compose les couches. Le CSS de l’en-tête bloque le rendu parce qu’il doit être complet avant d’être appliqué ; les scripts synchrones bloquent la lecture du document et attendent le CSS. L’ensemble de ces ressources et des étapes qui les relient est le chemin critique, et sa longueur détermine le moment du premier rendu.
Toutes les principales techniques d’optimisation du chargement, du CSS critique aux attributs defer et async, du preload au cache de page, sont des façons de raccourcir ce chemin : moins de ressources critiques, plus légères, avec moins d’étapes de l’une à l’autre. Le comprendre n’exige pas de devenir développeur, mais permet de lire un rapport de performance en sachant pourquoi chaque suggestion compte, et laquelle compte le plus.