Découvrez ce qu'est le scanner de préchargement du navigateur, comment il améliore les performances et comment l'éviter.
Pour optimiser la vitesse des pages, il est important de connaître certains aspects internes des navigateurs, qui sont souvent négligés. Les navigateurs effectuent certaines optimisations pour améliorer les performances d'une manière qui nous est impossible en tant que développeurs, mais seulement si ces optimisations ne sont pas involontairement contrariées.
L'une des optimisations internes du navigateur à comprendre est le scanner de préchargement du navigateur. Cet article explique le fonctionnement du scanner de préchargement et, surtout, comment éviter de le gêner.
Qu'est-ce qu'un outil d'analyse de préchargement ?
Chaque navigateur dispose d'un analyseur HTML principal qui tokenise le balisage brut et le traite pour le transformer en modèle objet. Tout se déroule à merveille jusqu'à ce que l'analyseur s'arrête lorsqu'il trouve une ressource bloquante, comme une feuille de style chargée avec un élément <link> ou un script chargé avec un élément <script> sans attribut async ni defer.
<link> pour un fichier CSS externe, ce qui empêche le navigateur d'analyser le reste du document, voire de l'afficher, tant que le CSS n'est pas téléchargé et analysé.
Dans le cas des fichiers CSS, le rendu est bloqué afin d'éviter un flash de contenu non stylisé (FOUC), qui se produit lorsqu'une version non stylisée d'une page peut être vue brièvement avant que les styles ne lui soient appliqués.
Le navigateur bloque également l'analyse et l'affichage de la page lorsqu'il rencontre des éléments <script> sans attribut defer ni async.
En effet, le navigateur ne peut pas savoir avec certitude si un script donné modifiera le DOM pendant que l'analyseur HTML principal effectue toujours son travail. C'est pourquoi il est courant de charger votre code JavaScript à la fin du document afin que les effets du blocage de l'analyse et du rendu deviennent marginaux.
Ce sont de bonnes raisons pour lesquelles le navigateur doit bloquer l'analyse et le rendu. Toutefois, il n'est pas souhaitable de bloquer l'une ou l'autre de ces étapes importantes, car elles peuvent retarder la découverte d'autres ressources importantes et donc le déroulement du spectacle. Heureusement, les navigateurs font de leur mieux pour atténuer ces problèmes grâce à un analyseur HTML secondaire appelé scanner de préchargement.
<body>. Toutefois, le scanner de préchargement peut anticiper le balisage brut pour trouver cette ressource d'image et commencer à la charger avant que l'analyseur HTML principal ne soit débloqué.
Le rôle d'un scanner de préchargement est spéculatif, ce qui signifie qu'il examine le balisage brut afin de trouver les ressources à récupérer de manière opportuniste avant que l'analyseur HTML principal ne les découvre.
Savoir quand le scanner de préchargement fonctionne
Le scanner de préchargement existe en raison du blocage du rendu et de l'analyse. Si ces deux problèmes de performances n'existaient pas, le préchargement ne serait pas très utile. Pour déterminer si une page Web bénéficie du scanner de préchargement, il faut tenir compte de ces phénomènes de blocage. Pour ce faire, vous pouvez introduire un délai artificiel pour les requêtes afin de déterminer où le scanner de préchargement fonctionne.
Prenons l'exemple de cette page contenant du texte et des images de base avec une feuille de style. Étant donné que les fichiers CSS bloquent le rendu et l'analyse, vous introduisez un délai artificiel de deux secondes pour la feuille de style via un service proxy. Ce délai permet de voir plus facilement dans la cascade réseau où le scanner de préchargement fonctionne.
Comme vous pouvez le voir dans la cascade, le scanner de préchargement découvre l'élément <img> même lorsque le rendu et l'analyse du document sont bloqués. Sans cette optimisation, le navigateur ne peut pas récupérer les éléments de manière opportuniste pendant la période de blocage, et davantage de demandes de ressources seraient consécutives plutôt que simultanées.
Maintenant que nous avons vu cet exemple simple, examinons quelques schémas concrets où le scanner de préchargement peut être contourné, et ce qui peut être fait pour les corriger.
Scripts async injectés
Supposons que vous ayez du code HTML dans votre <head> qui inclut du code JavaScript intégré comme ceci :
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Les scripts injectés sont async par défaut. Par conséquent, lorsque ce script est injecté, il se comporte comme si l'attribut async lui avait été appliqué. Cela signifie qu'il s'exécutera dès que possible et ne bloquera pas le rendu. Cela semble optimal, n'est-ce pas ? Toutefois, si vous supposez que ce <script> intégré suit un élément <link> qui charge un fichier CSS externe, vous obtiendrez un résultat non optimal :
async injecté. Le scanner de préchargement ne peut pas détecter le script pendant la phase de blocage de l'affichage, car il est injecté sur le client.
Voici ce qui s'est passé :
- À 0 seconde, le document principal est demandé.
- À 1,4 seconde, le premier octet de la requête de navigation arrive.
- À 2,0 secondes, le CSS et l'image sont demandés.
- Étant donné que l'analyseur est bloqué lors du chargement de la feuille de style et que le code JavaScript intégré qui injecte le script
asyncarrive après cette feuille de style à 2,6 secondes, la fonctionnalité fournie par ce script n'est pas disponible aussi rapidement qu'elle pourrait l'être.
Cette approche n'est pas optimale, car la requête du script n'est effectuée qu'une fois la feuille de style téléchargée. Cela retarde l'exécution du script dès que possible. En revanche, l'élément <img> étant détectable dans le balisage fourni par le serveur, il est découvert par le scanner de préchargement.
Que se passe-t-il si vous utilisez une balise <script> standard avec l'attribut async au lieu d'injecter le script dans le DOM ?
<script src="/yall.min.js" async></script>
Voici le résultat :
async <script>. Le scanner de préchargement découvre le script pendant la phase de blocage de l'affichage et le charge en même temps que le CSS.
Vous pourriez être tenté de suggérer que ces problèmes peuvent être résolus en utilisant rel=preload. Cela fonctionnerait certainement, mais cela pourrait avoir des effets secondaires. Après tout, pourquoi utiliser rel=preload pour résoudre un problème qui peut être évité en n'injectant pas un élément <script> dans le DOM ?
async injecté, mais le script async est préchargé pour s'assurer qu'il est découvert plus tôt.
Le préchargement "corrige" le problème ici, mais il en introduit un nouveau : le script async des deux premières démos, bien que chargé dans le <head>, est chargé avec une priorité "Faible", tandis que la feuille de style est chargée avec une priorité "Très élevée". Dans la dernière démo où le script async est préchargé, la feuille de style est toujours chargée avec la priorité "Highest" (Très élevée), mais la priorité du script a été définie sur "High" (Élevée).
Lorsque la priorité d'une ressource est augmentée, le navigateur lui alloue plus de bande passante. Cela signifie que, même si la feuille de style a la priorité la plus élevée, la priorité accrue du script peut entraîner une contention de bande passante. Cela peut être un facteur de ralentissement des connexions ou dans les cas où les ressources sont assez volumineuses.
La réponse ici est simple : si un script est nécessaire au démarrage, ne contournez pas le scanner de préchargement en l'injectant dans le DOM. Faites des tests si nécessaire avec l'emplacement de l'élément <script>, ainsi qu'avec des attributs tels que defer et async.
Chargement différé avec JavaScript
Le chargement différé est une excellente méthode pour économiser des données, souvent appliquée aux images. Toutefois, il arrive que le chargement différé soit appliqué de manière incorrecte aux images qui se trouvent "au-dessus de la ligne de flottaison".
Cela peut entraîner des problèmes de détectabilité des ressources pour le scanner de préchargement et retarder inutilement la découverte, le téléchargement, le décodage et l'affichage d'une référence à une image. Prenons cet exemple de balisage d'image :
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
L'utilisation d'un préfixe data- est un modèle courant dans les chargeurs différés optimisés par JavaScript. Lorsque l'image est affichée dans la fenêtre d'affichage, le chargeur différé supprime le préfixe data-. Dans l'exemple précédent, data-src devient donc src. Cette mise à jour invite le navigateur à récupérer la ressource.
Ce modèle n'est pas problématique tant qu'il n'est pas appliqué aux images qui se trouvent dans la fenêtre d'affichage au démarrage. Étant donné que le scanner de préchargement ne lit pas l'attribut data-src de la même manière qu'un attribut src (ou srcset), la référence d'image n'est pas découverte plus tôt. Pire encore, le chargement de l'image est retardé jusqu'après le téléchargement, la compilation et l'exécution du JavaScript du chargeur différé.
Selon la taille de l'image (qui peut dépendre de la taille de la fenêtre d'affichage), elle peut être un élément candidat pour le Largest Contentful Paint (LCP). Lorsque le scanner de préchargement ne peut pas récupérer la ressource d'image de manière spéculative à l'avance(peut-être au moment où la ou les feuilles de style de la page bloquent le rendu), le temps LCP en souffre.
La solution consiste à modifier le balisage de l'image :
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Il s'agit du modèle optimal pour les images qui se trouvent dans la fenêtre d'affichage au démarrage, car le scanner de préchargement découvrira et récupérera la ressource d'image plus rapidement.
Dans cet exemple simplifié, le résultat est une amélioration de 100 millisecondes du temps LCP sur une connexion lente. Cela ne semble pas être une amélioration énorme, mais elle l'est si l'on considère que la solution est une correction rapide du balisage et que la plupart des pages Web sont plus complexes que cet ensemble d'exemples. Cela signifie que les candidats LCP peuvent avoir à se disputer la bande passante avec de nombreuses autres ressources. Les optimisations comme celle-ci deviennent donc de plus en plus importantes.
Images de fond CSS
N'oubliez pas que le scanner de préchargement du navigateur analyse le balisage. Il n'analyse pas les autres types de ressources, comme CSS, qui peuvent impliquer des récupérations d'images référencées par la propriété background-image.
Comme pour le HTML, les navigateurs traitent le CSS dans son propre modèle d'objet, appelé CSSOM. Si des ressources externes sont découvertes lors de la construction du CSSOM, elles sont demandées au moment de la découverte, et non par le préchargement.
Imaginons que le candidat LCP de votre page soit un élément avec une propriété CSS background-image. Voici ce qui se passe lorsque les ressources se chargent :
background-image (ligne 3). La récupération de l'image demandée ne commence que lorsque l'analyseur CSS la trouve.
Dans ce cas, le scanner de préchargement n'est pas vraiment vaincu, mais plutôt non impliqué. Toutefois, si un candidat LCP sur la page provient d'une propriété CSS background-image, vous devez précharger cette image :
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Cette petite indication rel=preload permet au navigateur de découvrir l'image plus tôt que d'habitude :
background-image (ligne 3). L'indication rel=preload aide le navigateur à découvrir l'image environ 250 millisecondes plus tôt que sans cette indication.
Avec l'indication rel=preload, le candidat LCP est découvert plus tôt, ce qui réduit le temps LCP. Bien que cet indice permette de résoudre ce problème, la meilleure option peut être d'évaluer si votre candidat LCP d'image doit être chargé à partir du CSS. Avec une balise <img>, vous pouvez mieux contrôler le chargement d'une image adaptée à la fenêtre d'affichage tout en permettant au préchargement de la découvrir.
Trop de ressources intégrées
L'intégration consiste à placer une ressource à l'intérieur du code HTML. Vous pouvez insérer des feuilles de style dans des éléments <style>, des scripts dans des éléments <script> et pratiquement n'importe quelle autre ressource à l'aide de l'encodage Base64.
L'intégration des ressources peut être plus rapide que leur téléchargement, car aucune requête distincte n'est émise pour la ressource. Il se trouve directement dans le document et se charge instantanément. Cependant, cette approche présente des inconvénients importants :
- Si vous ne mettez pas en cache votre code HTML (ce qui est impossible si la réponse HTML est dynamique), les ressources intégrées ne sont jamais mises en cache. Cela affecte les performances, car les ressources intégrées ne sont pas réutilisables.
- Même si vous pouvez mettre en cache le code HTML, les ressources intégrées ne sont pas partagées entre les documents. Cela réduit l'efficacité de la mise en cache par rapport aux fichiers externes qui peuvent être mis en cache et réutilisés sur une origine entière.
- Si vous intégrez trop de contenu, vous empêchez le scanner de préchargement de découvrir les ressources plus loin dans le document, car le téléchargement de ce contenu intégré supplémentaire prend plus de temps.
Prenons cette page comme exemple. Dans certaines conditions, le candidat LCP est l'image en haut de la page, et le CSS se trouve dans un fichier distinct chargé par un élément <link>. La page utilise également quatre polices Web qui sont demandées en tant que fichiers distincts à partir de la ressource CSS.
<img>, mais il est détecté par le préchargement, car le CSS et les polices nécessaires au chargement de la page se trouvent dans des ressources distinctes, ce qui n'empêche pas le préchargement de faire son travail.
Que se passe-t-il si le CSS et toutes les polices sont intégrés en tant que ressources base64 ?
<img>, mais l'intégration du CSS et de ses quatre ressources de police dans le `` empêche le préchargeur de découvrir l'image tant que ces ressources ne sont pas entièrement téléchargées.
Dans cet exemple, l'impact de l'intégration a des conséquences négatives sur le LCP et sur les performances en général. La version de la page qui n'intègre rien en ligne affiche l'image LCP en 3,5 secondes environ. La page qui intègre tout n'affiche l'image LCP qu'au bout de sept secondes.
Il ne s'agit pas uniquement du scanner de préchargement. L'intégration des polices n'est pas une stratégie idéale, car le format base64 n'est pas efficace pour les ressources binaires. Un autre facteur en jeu est que les ressources de police externes ne sont téléchargées que si le CSSOM les juge nécessaires. Lorsque ces polices sont intégrées en tant que base64, elles sont téléchargées, qu'elles soient nécessaires ou non pour la page actuelle.
Un préchargement pourrait-il améliorer les choses ici ? Bien sûr. Vous pourriez précharger l'image LCP et réduire le temps LCP, mais l'ajout de ressources intégrées à votre code HTML potentiellement non cachable a d'autres conséquences négatives sur les performances. Le First Contentful Paint (FCP) est également concerné par ce schéma. Dans la version de la page où rien n'est intégré, le FCP est d'environ 2,7 secondes. Dans la version où tout est intégré, le FCP est d'environ 5,8 secondes.
Faites très attention à l'intégration de contenu dans le code HTML, en particulier les ressources encodées en base64. En général, cette option n'est pas recommandée, sauf pour les très petites ressources. Intégrez le moins possible de contenu, car en intégrer trop est risqué.
Afficher le balisage avec JavaScript côté client
Il ne fait aucun doute que JavaScript a un impact sur la vitesse des pages. Non seulement les développeurs en dépendent pour fournir de l'interactivité, mais ils ont également tendance à s'en servir pour diffuser du contenu. Cela améliore l'expérience des développeurs à certains égards, mais les avantages pour les développeurs ne se traduisent pas toujours par des avantages pour les utilisateurs.
Un modèle qui peut déjouer le scanner de préchargement consiste à afficher le balisage avec JavaScript côté client :
Lorsque les charges utiles de balisage sont contenues dans JavaScript et rendues entièrement par celui-ci dans le navigateur, toutes les ressources de ce balisage sont effectivement invisibles pour le scanner de préchargement. Cela retarde la découverte des ressources importantes, ce qui affecte certainement le LCP. Dans ces exemples, la requête pour l'image LCP est considérablement retardée par rapport à l'expérience équivalente rendue côté serveur qui ne nécessite pas l'affichage de JavaScript.
Cela s'éloigne un peu de l'objectif de cet article, mais les effets du rendu du balisage sur le client vont bien au-delà de la désactivation du préchargement. Tout d'abord, l'introduction de JavaScript pour alimenter une expérience qui n'en a pas besoin introduit un temps de traitement inutile qui peut affecter l'interaction to Next Paint (INP). Le rendu d'une très grande quantité de balisage sur le client est plus susceptible de générer des tâches longues que la même quantité de balisage envoyée par le serveur. La raison de cela, en plus du traitement supplémentaire qu'implique JavaScript, est que les navigateurs diffusent le balisage à partir du serveur et fragmentent le rendu de manière à limiter les tâches longues. En revanche, le balisage rendu côté client est traité comme une tâche unique et monolithique, ce qui peut affecter l'INP d'une page.
La solution à ce scénario dépend de la réponse à la question suivante : Existe-t-il une raison pour laquelle le balisage de votre page ne peut pas être fourni par le serveur au lieu d'être affiché sur le client ? Si la réponse est "non", envisagez le rendu côté serveur (SSR) ou le balisage généré de manière statique dans la mesure du possible. Cela aidera le scanner de préchargement à découvrir et à récupérer de manière opportuniste les ressources importantes à l'avance.
Si votre page a besoin de JavaScript pour associer des fonctionnalités à certaines parties du balisage de votre page, vous pouvez toujours le faire avec le SSR, soit avec du JavaScript pur, soit avec l'hydratation pour obtenir le meilleur des deux mondes.
Aidez le scanner de préchargement à vous aider
Le scanner de préchargement est une optimisation de navigateur très efficace qui permet aux pages de se charger plus rapidement au démarrage. En évitant les schémas qui empêchent le navigateur de découvrir les ressources importantes à l'avance, vous simplifiez non seulement le développement, mais vous créez également de meilleures expériences utilisateur qui généreront de meilleurs résultats pour de nombreuses métriques, y compris certains signaux Web.
Pour résumer, voici les principaux points à retenir de cet article :
- Le scanner de préchargement du navigateur est un analyseur HTML secondaire qui analyse en amont de l'analyseur principal s'il est bloqué, afin de découvrir de manière opportuniste les ressources qu'il peut récupérer plus tôt.
- Le scanner de préchargement ne peut pas découvrir les ressources qui ne sont pas présentes dans le balisage fourni par le serveur lors de la requête de navigation initiale. Voici quelques exemples de méthodes permettant de contourner le scanner de préchargement (liste non exhaustive) :
- Injecter des ressources dans le DOM avec JavaScript, qu'il s'agisse de scripts, d'images, de feuilles de style ou de tout autre élément qui serait mieux placé dans la charge utile de balisage initiale du serveur.
- Chargement différé des images ou des iFrames au-dessus de la ligne de flottaison à l'aide d'une solution JavaScript.
- Affichage du balisage sur le client, qui peut contenir des références à des sous-ressources de document à l'aide de JavaScript.
- Le scanner de préchargement n'analyse que le code HTML. Il n'examine pas le contenu des autres ressources, en particulier le CSS, qui peuvent inclure des références à des éléments importants, y compris les candidats LCP.
Si, pour une raison quelconque, vous ne pouvez pas éviter un modèle qui affecte négativement la capacité du scanner de préchargement à accélérer les performances de chargement, envisagez d'utiliser l'indice de ressource rel=preload. Si vous utilisez rel=preload, effectuez des tests dans les outils de laboratoire pour vous assurer qu'il produit l'effet souhaité. Enfin, ne préchargez pas trop de ressources, car si vous donnez la priorité à tout, rien ne le sera.
Ressources
- Les scripts asynchrones insérés à l'aide d'un script sont considérés comme dangereux
- Comment le préchargeur de navigateur accélère le chargement des pages
- Précharger les composants critiques pour améliorer la vitesse de chargement
- Établissez des connexions réseau tôt pour améliorer la vitesse perçue des pages
- Optimiser le Largest Contentful Paint
Image principale d'Unsplash, par Mohammad Rahmani .