Découvrez ce qu'est le scanner de préchargement du navigateur, comment il améliore les performances et comment vous pouvez l'éviter.
Un aspect souvent négligé de l'optimisation de la vitesse des pages consiste à connaître un peu le fonctionnement interne des navigateurs. Les navigateurs effectuent certaines optimisations pour améliorer les performances d'une manière que nous, en tant que développeurs, ne pouvons pas faire, mais seulement tant que ces optimisations ne sont pas involontairement contrecarrées.
Le scanner de préchargement du navigateur est une optimisation interne du navigateur à comprendre. Cet article explique le fonctionnement du scanner de préchargement et, plus important encore, comment éviter de l'entraver.
Qu'est-ce qu'un scanner de préchargement ?
Chaque navigateur dispose d'un analyseur HTML principal qui tokenise le balisage brut et le traite dans un modèle d'objet. Tout se passe bien jusqu'à ce que l'analyseur s'arrête lorsqu'il trouve une ressource bloquante, telle qu'une feuille de style chargée avec un élément <link> ou un script chargé avec un élément <script> sans attribut async ou defer.
<link> pour un fichier CSS externe, ce qui empêche le navigateur d'analyser le reste du document (ou même d'en afficher une partie) jusqu'à ce que le CSS soit téléchargé et analysé.
Dans le cas des fichiers CSS, l'affichage est bloqué pour éviter un flash de contenu sans style (FOUC), c'est-à-dire lorsqu'une version sans style d'une page peut être brièvement visible 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 ou 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 de l'affichage deviennent marginaux.
Ce sont de bonnes raisons pour lesquelles le navigateur devrait bloquer l'analyse et l'affichage. Cependant, il n'est pas souhaitable de bloquer l'une de ces étapes importantes, car cela peut retarder la découverte d'autres ressources importantes. 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, l'analyseur 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 des ressources à récupérer de manière opportuniste avant que l'analyseur HTML principal ne les découvre.
Comment savoir si le scanner de préchargement fonctionne ?
Le scanner de préchargement existe en raison du blocage de l'affichage et de l'analyse. Si ces deux problèmes de performances n'existaient pas, le scanner de préchargement ne serait pas très utile. Pour déterminer si une page Web bénéficie du scanner de préchargement, il est essentiel de comprendre 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 à la fois l'affichage 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 où le scanner de préchargement fonctionne dans la cascade du réseau.
Comme vous pouvez le voir dans le graphique en cascade, le scanner de préchargement découvre l'élément <img> même lorsque l'affichage 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 requêtes de ressources seraient consécutives plutôt que simultanées.
Maintenant que nous avons vu cet exemple simple, examinons quelques modèles réels dans lesquels le scanner de préchargement peut être contourné, et ce qui peut être fait pour y remédier.
async scripts injectés
Supposons que votre <head> contienne du code HTML 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. Ainsi, lorsque ce script est injecté, il se comporte comme si l'attribut async lui était appliqué. Cela signifie qu'il s'exécutera dès que possible et ne bloquera pas l'affichage. Cela semble optimal, n'est-ce pas ? Cependant, si vous supposez que cet élément <script> intégré vient après 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 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 le pourrait.
Cette approche n'est pas optimale, car la requête pour le script n'est envoyée qu'une fois le téléchargement de la feuille de style terminé. Cela retarde l'exécution du script dès que possible. En revanche, comme l'élément <img> est détectable dans le balisage fourni par le serveur, il est détecté par l'analyseur 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.
On pourrait être tenté de suggérer que ces problèmes pourraient ê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 d'é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 "résout" le problème ici, mais il en introduit un nouveau : le script async des deux premières démonstrations, bien qu'il soit chargé dans le <head>, est chargé avec une priorité "Faible", tandis que la feuille de style est chargée avec une priorité "La plus élevée". Dans la dernière démonstration où le script async est préchargé, la feuille de style est toujours chargée avec la priorité "La plus élevée", mais la priorité du script a été promue à "É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 la bande passante. Cela peut être un facteur sur les connexions lentes ou lorsque les ressources sont assez volumineuses.
La réponse 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 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. Cependant, 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", pour ainsi dire.
Cela peut entraîner des problèmes de visibilité des ressources pour le scanner de préchargement, et retarder inutilement le temps nécessaire pour découvrir une référence à une image, la télécharger, la décoder et la présenter. Prenons l'exemple de ce 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 basés sur 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 ne pose pas de problème tant qu'il n'est pas appliqué aux images qui se trouvent dans la fenêtre d'affichage au démarrage. Comme 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 de l'image n'est pas découverte plus tôt. Pire encore, le chargement de l'image est retardé après le téléchargement, la compilation et l'exécution du JavaScript de chargement différé.
Selon la taille de l'image (qui peut dépendre de la taille de la fenêtre d'affichage), il peut s'agir d'un élément candidat pour le Largest Contentful Paint (LCP). Lorsque le scanner de préchargement ne peut pas récupérer de manière spéculative la ressource d'image à l'avance(par exemple, lorsque la ou les feuilles de style de la page bloquent l'affichage), le LCP en pâtit.
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 l'analyseur de préchargement découvre et récupère la ressource d'image plus rapidement.
Dans cet exemple simplifié, le LCP est amélioré de 100 millisecondes sur une connexion lente. Cela peut ne pas sembler être une amélioration majeure, mais c'est le cas 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 de ce type 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 les fichiers CSS, qui peuvent impliquer la récupération d'images référencées par la propriété background-image.
Comme pour le code HTML, les navigateurs traitent le code CSS dans leur 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 l'analyseur de 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 lors du chargement des ressources :
background-image (ligne 3). L'image qu'il demande ne commence à être récupérée que lorsque l'analyseur CSS la trouve.
Dans ce cas, le scanner de préchargement n'est pas tant vaincu qu'il n'est pas 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">
Cet indice rel=preload est petit, mais il aide le navigateur à découvrir l'image plus tôt qu'il ne le ferait autrement :
background-image (ligne 3). L'indice rel=preload aide le navigateur à découvrir l'image environ 250 millisecondes plus tôt que sans l'indice.
Avec l'indice rel=preload, le candidat LCP est découvert plus tôt, ce qui réduit le temps LCP. Bien que cette indication permette de résoudre ce problème, la meilleure option peut être d'évaluer si votre image candidate LCP doit être chargée à partir du CSS. Avec une balise <img>, vous aurez plus de contrôle sur le chargement d'une image adaptée à la fenêtre d'affichage, tout en permettant au scanner de préchargement de la découvrir.
Intégration d'un trop grand nombre de ressources
L'intégration consiste à placer une ressource dans le code HTML. Vous pouvez intégrer 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 de ressources peut être plus rapide que leur téléchargement, car aucune requête distincte n'est émise pour la ressource. Il est intégré au document et se charge instantanément. Cependant, il existe 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 l'ensemble d'une origine.
- Si vous intégrez trop de ressources, vous retardez la découverte des ressources ultérieures dans le document par le scanner de préchargement, car le téléchargement de ce contenu supplémentaire intégré 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 sous forme de fichiers distincts de la ressource CSS.
<img>, mais il est détecté par l'analyseur de préchargement, car le CSS et les polices requis pour la page se chargent dans des ressources distinctes, ce qui n'empêche pas l'analyseur de 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 scanner de préchargement 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 affiche l'image LCP en environ 3,5 secondes. La page qui intègre tout n'affiche l'image LCP qu'au bout d'un peu plus de sept secondes.
Le scanner de préchargement n'est pas le seul élément en jeu ici. L'intégration des polices n'est pas une bonne stratégie, car base64 est un format inefficace pour les ressources binaires. Un autre facteur entre en jeu : les ressources de polices externes ne sont pas téléchargées, sauf si le CSSOM les juge nécessaires. Lorsque ces polices sont intégrées au format 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 le fait de surcharger votre code HTML potentiellement non mis en cache avec des ressources intégrées a d'autres conséquences négatives sur les performances. Le First Contentful Paint (FCP) est également affecté par ce modèle. 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 d'éléments dans le code HTML, en particulier les ressources encodées en base64. En général, cela n'est pas recommandé, sauf pour les très petites ressources. Incorporez le moins possible, car une incorporation excessive est risquée.
Afficher le balisage avec du code JavaScript côté client
Cela ne fait aucun doute : 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'appuyer sur lui pour diffuser le contenu lui-même. 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 contourner le scanner de préchargement consiste à afficher le balisage avec du code JavaScript côté client :
Lorsque les charges utiles de balisage sont contenues dans JavaScript et affichées entièrement par JavaScript dans le navigateur, toutes les ressources de ce balisage sont invisibles pour le scanner de préchargement. Cela retarde la découverte de 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 affichée par le 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 neutralisation du scanner de préchargement. D'une part, l'introduction de JavaScript pour optimiser une expérience qui n'en a pas besoin entraîne un temps de traitement inutile qui peut affecter l'Interaction to Next Paint (INP). Le rendu d'une quantité extrêmement importante 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, outre le traitement supplémentaire qu'implique JavaScript, est que les navigateurs diffusent le balisage depuis le serveur et découpent l'affichage de manière à limiter les tâches longues. En revanche, le balisage affiché par le client est géré comme une seule tâche monolithique, ce qui peut affecter l'INP d'une page.
La solution à ce problème dépend de la réponse à la question suivante : Y a-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 d'utiliser le rendu côté serveur (SSR) ou le balisage généré de manière statique, car cela aidera l'analyseur de préchargement à découvrir et à récupérer les ressources importantes à l'avance.
Si votre page a besoin de JavaScript pour associer des fonctionnalités à certaines parties de son balisage, vous pouvez toujours le faire avec le SSR, soit avec du JavaScript vanilla, soit avec l'hydratation pour profiter du 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 de charger les pages plus rapidement au démarrage. En évitant les modèles qui l'empêchent de découvrir des ressources importantes à l'avance, vous simplifiez non seulement le développement, mais vous créez également de meilleures expériences utilisateur qui donneront de meilleurs résultats dans de nombreuses métriques, y compris certains signaux Web essentiels.
Pour résumer, voici les points à retenir de cet article :
- Le scanner de préchargement du navigateur est un analyseur HTML secondaire qui analyse avant l'analyseur principal s'il est bloqué pour découvrir de manière opportuniste les ressources qu'il peut récupérer plus tôt.
- Les ressources qui ne sont pas présentes dans le balisage fourni par le serveur lors de la requête de navigation initiale ne peuvent pas être détectées par le scanner de préchargement. Voici quelques exemples de cas où le scanner de préchargement peut être contourné (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 aux sous-ressources du document à l'aide de JavaScript.
- Le scanner de préchargement n'analyse que le code HTML. Il n'examine pas le contenu d'autres ressources (en particulier CSS) qui peuvent inclure des références à des assets importants, y compris des 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, testez-le dans les outils de laboratoire pour vous assurer qu'il produit l'effet souhaité. Enfin, ne préchargez pas trop de ressources, car lorsque vous donnez la priorité à tout, rien ne l'est.
Ressources
- Les scripts asynchrones insérés à l'aide d'un script sont considérés comme dangereux
- How the Browser Pre-loader Makes Pages Load Faster
- Précharger les composants critiques pour améliorer la vitesse de chargement
- Établir des connexions réseau de manière anticipée pour améliorer la vitesse perçue de la page
- Optimiser le Largest Contentful Paint