Fonctionnement interne d'un processus de moteur de rendu
Il s'agit de la troisième partie sur quatre de cette série d'articles de blog consacrée au fonctionnement des navigateurs. Nous avons précédemment abordé l'architecture multiprocessus et le flux de navigation. Dans cet article, nous allons voir ce qui se passe à l'intérieur du processus du moteur de rendu.
Le processus du moteur de rendu a un impact sur de nombreux aspects des performances Web. Étant donné qu'il se passe beaucoup de choses au sein du processus du moteur de rendu, cet article n'est qu'une présentation générale. Si vous souhaitez obtenir des informations plus détaillées, la section Performances de Web Fundamentals propose de nombreuses autres ressources.
Les processus du moteur de rendu gèrent les contenus Web
Le processus de moteur de rendu est responsable de tout ce qui se passe dans un onglet. Dans un processus de moteur de rendu, le thread principal gère la majeure partie du code que vous envoyez à l'utilisateur. Parfois, certaines parties de votre code JavaScript sont gérées par des threads de nœud de calcul si vous utilisez un nœud de calcul Web ou un service worker. Les threads de compositeur et de trame sont également exécutés dans les processus d'un moteur de rendu pour afficher une page de manière efficace et fluide.
La tâche principale du processus de rendu consiste à transformer HTML, CSS et JavaScript en une page Web avec laquelle l'utilisateur peut interagir.
analyse
Construction d'un DOM
Lorsque le processus du moteur de rendu reçoit un message de commit pour une navigation et commence à recevoir des données HTML, le thread principal commence à analyser la chaîne de texte (HTML) et la transforme en Document Object Model (DOM).
Le DOM est une représentation interne de la page pour un navigateur, ainsi que la structure des données et l'API avec lesquelles les développeurs Web peuvent interagir via JavaScript.
L'analyse d'un document HTML dans un DOM est définie par la norme HTML. Vous avez peut-être remarqué que l'envoi de code HTML à un navigateur ne génère jamais d'erreur. Par exemple, l'absence de la balise de fermeture </p>
est un code HTML valide. Un balisage incorrect de type Hi! <b>I'm <i>Chrome</b>!</i>
(la balise b est fermée avant la balise i) est traité comme si vous écriviez Hi! <b>I'm <i>Chrome</i></b><i>!</i>
. En effet, la spécification HTML est conçue pour gérer ces erreurs de manière optimale. Si vous souhaitez en savoir plus sur la procédure à suivre, vous pouvez lire la section An introduction to error training and étrange cas dans l'analyseur de la spécification HTML.
Chargement de la sous-ressource
Un site Web utilise généralement des ressources externes telles que des images, CSS et JavaScript. Ces fichiers doivent être chargés à partir du réseau ou du cache. Le thread principal pourrait les demander l'un après l'autre au fur et à mesure qu'ils les trouvent lors de l'analyse pour créer un DOM, mais pour accélérer, un "outil d'analyse de préchargement" est exécuté simultanément.
Si le document HTML contient des éléments tels que <img>
ou <link>
, l'outil d'analyse de préchargement examine les jetons générés par l'analyseur HTML et envoie les requêtes au thread réseau dans le processus du navigateur.
JavaScript peut bloquer l'analyse
Lorsque l'analyseur HTML trouve une balise <script>
, il interrompt l'analyse du document HTML, et doit charger, analyser et exécuter le code JavaScript. Pourquoi ? Parce que JavaScript peut modifier la forme du document à l'aide d'éléments tels que document.write()
, qui modifie l'ensemble de la structure DOM (la présentation du modèle d'analyse dans la spécification HTML présente un beau schéma). C'est pourquoi l'analyseur HTML doit attendre que JavaScript soit exécuté avant de pouvoir reprendre l'analyse du document HTML. Si vous souhaitez savoir ce qu'il se passe lors de l'exécution JavaScript, l'équipe V8 discute et publie des articles de blog à ce sujet.
Indice pour indiquer la manière dont vous souhaitez charger les ressources dans le navigateur
Il existe de nombreuses façons pour les développeurs Web d'envoyer des indications au navigateur afin de charger correctement les ressources.
Si votre code JavaScript n'utilise pas document.write()
, vous pouvez ajouter l'attribut async
ou defer
à la balise <script>
. Ensuite, le navigateur charge et exécute le code JavaScript de manière asynchrone et ne bloque pas l'analyse. Vous pouvez également utiliser le module JavaScript si besoin. <link rel="preload">
permet d'informer le navigateur que la ressource est absolument nécessaire pour la navigation actuelle et que vous souhaitez la télécharger dès que possible. Pour en savoir plus, consultez Hiérarchisation des ressources : comment le navigateur vous aide.
Calcul du style
Avoir un DOM ne suffit pas pour savoir à quoi ressemblerait la page, car nous pouvons styliser les éléments de la page en CSS. Le thread principal analyse le code CSS et détermine le style calculé pour chaque nœud DOM. Il s'agit d'informations sur le type de style appliqué à chaque élément en fonction des sélecteurs CSS. Vous pouvez consulter ces informations dans la section computed
des outils de développement.
Même si vous ne fournissez aucun code CSS, chaque nœud DOM possède un style calculé. La taille de la balise <h1>
est supérieure à celle de la balise <h2>
, et des marges sont définies pour chaque élément. En effet, le navigateur dispose d'une feuille de style par défaut. Si vous voulez savoir à quoi ressemble le CSS par défaut de Chrome, vous pouvez consulter le code source ici.
Mise en page
Le processus du moteur de rendu connaît maintenant la structure d'un document et les styles de chaque nœud, mais cela ne suffit pas pour afficher une page. Imaginez que vous essayez de décrire un tableau à un ami sur un téléphone. "Il y a un grand cercle rouge et un petit carré bleu" ne suffit pas pour que votre ami sache à quoi ressemblerait exactement le tableau.
La mise en page est un processus permettant de trouver la géométrie des éléments. Le thread principal parcourt les styles DOM et calculés et crée l'arborescence de mise en page qui contient des informations telles que les coordonnées X et les tailles du cadre de délimitation. L'arborescence de mise en page peut être semblable à l'arborescence DOM, mais ne contient que des informations liées à ce qui est visible sur la page. Si display: none
est appliqué, cet élément ne fait pas partie de l'arborescence de mise en page (toutefois, un élément avec visibility: hidden
se trouve dans l'arborescence de mise en page). De même, si une pseudo-classe avec du contenu tel que p::before{content:"Hi!"}
est appliquée, elle est incluse dans l'arborescence de mise en page, même si elle ne se trouve pas dans le DOM.
Déterminer la mise en page d'une page est une tâche difficile. Même la mise en page la plus simple, comme un bloc de flux de haut en bas, doit tenir compte de la taille de la police et de l'emplacement des sauts de ligne, car ceux-ci affectent la taille et la forme d'un paragraphe, ce qui affecte ensuite l'emplacement du paragraphe suivant.
CSS peut faire flotter un élément sur un côté, masquer l'élément en trop et modifier la direction d'écriture. Comme vous pouvez l'imaginer, cette étape de mise en page comporte une tâche importante. Dans Chrome, toute une équipe d'ingénieurs travaille sur la mise en page. Si vous souhaitez voir les détails de leur travail, quelques conférences de la conférence BlinkOn sont enregistrées et très intéressantes à regarder.
Peindre
Un DOM, un style et une mise en page ne suffisent pas pour afficher une page. Disons que vous essayez de reproduire un tableau. Vous connaissez la taille, la forme et l'emplacement des éléments, mais vous devez toujours juger dans quel ordre vous les peignez.
Par exemple, z-index
peut être défini pour certains éléments. Dans ce cas, si vous affichez les éléments dans l'ordre des éléments écrits dans le code HTML, cela entraînera un affichage incorrect.
À cette étape de peinture, le thread principal parcourt l'arborescence de mise en page pour créer des enregistrements de peinture. L'enregistrement de peinture est une note du processus de peinture comme "l'arrière-plan d'abord, puis le texte, puis le rectangle". Si vous avez dessiné un élément <canvas>
à l'aide de JavaScript, ce processus vous est peut-être familier.
La mise à jour du pipeline de rendu est coûteuse
Dans le pipeline de rendu, il est essentiel de comprendre qu'à chaque étape, le résultat de l'opération précédente est utilisé pour créer de nouvelles données. Par exemple, si quelque chose change dans l'arborescence de mise en page, l'ordre "Pain" doit être généré de nouveau pour les parties concernées du document.
Si vous animez des éléments, le navigateur doit effectuer ces opérations entre chaque frame. La plupart de nos écrans actualisent l'écran 60 fois par seconde (60 FPS). L'animation est fluide pour les yeux de l'homme lorsque vous déplacez des éléments à l'écran à chaque image. Toutefois, si l'animation manque les images intermédiaires, la page apparaîtra "à-coup".
Même si vos opérations de rendu suivent l'actualisation de l'écran, ces calculs s'exécutent sur le thread principal, ce qui signifie qu'il peut être bloqué lorsque votre application exécute JavaScript.
Vous pouvez diviser l'opération JavaScript en petits fragments et planifier son exécution sur chaque frame à l'aide de requestAnimationFrame()
. Pour en savoir plus à ce sujet, consultez la page Optimiser l'exécution JavaScript. Vous pouvez également exécuter votre code JavaScript dans les workers Web pour éviter de bloquer le thread principal.
Composition
Comment dessineriez-vous une page ?
Maintenant que le navigateur connaît la structure du document, le style de chaque élément, la géométrie de la page et l'ordre de rendu, comment dessine-t-il une page ? La transformation de ces informations en pixels à l'écran s'appelle la rastérisation.
Pour résoudre ce problème, il peut être plus simple d'effectuer une trame dans la fenêtre d'affichage. Si l'utilisateur fait défiler la page, déplacez le cadre matriciel et remplissez les parties manquantes en effectuant d'autres trames. Voici comment Chrome gérait la rastérisation lors de sa première publication. Cependant, les navigateurs récents exécute un processus plus sophistiqué appelé "composition".
Qu'est-ce que la composition
La composition est une technique qui permet de diviser les parties d'une page en calques, de les rastériser séparément et de composer une page dans un thread distinct appelé thread du compositeur. En cas de défilement, étant donné que les calques sont déjà rastérisés, il suffit de créer une composition graphique. L'animation peut être réalisée de la même manière en déplaçant des calques et en créant un composite d'une nouvelle image.
Vous pouvez voir comment votre site Web est divisé en couches dans les Outils de développement à l'aide du panneau Calques.
Diviser en calques
Pour savoir quels éléments doivent figurer dans quelles couches, le thread principal parcourt l'arborescence de mise en page pour créer l'arborescence des calques (cette partie est appelée "Mettre à jour l'arborescence des calques" dans le panneau des performances des outils de développement). Si certaines parties d'une page qui doivent constituer une couche distincte (comme le menu latéral coulissant) n'en reçoivent pas, vous pouvez fournir des indications au navigateur à l'aide de l'attribut will-change
en CSS.
Vous pourriez être tenté d'attribuer des couches à chaque élément, mais la composition d'un nombre excessif de couches peut ralentir le processus que la rastérisation de petites parties d'une page à chaque frame. Il est donc essentiel de mesurer les performances d'affichage de votre application. Pour en savoir plus à ce sujet, consultez S'en tenir aux propriétés de type compositeur uniquement et gérer le nombre de calques.
Trame et composite à partir du thread principal
Une fois l'arborescence des couches créée et les commandes de peinture déterminées, le thread principal valide ces informations dans le thread du compositeur. Le thread du compositeur rastérise ensuite chaque calque. Un calque peut représenter toute la longueur d'une page. Le thread compositeur les divise en tuiles et envoie chaque tuile aux threads matriciels. Les threads de trame rastérisent chaque carte et les stockent dans la mémoire du GPU.
Le thread du compositeur peut donner la priorité à différents threads matriciels afin que les éléments situés dans la fenêtre d'affichage (ou à proximité) puissent être rastérisés en premier. Un calque comporte également plusieurs tuiles pour différentes résolutions afin de gérer des éléments tels que le zoom avant.
Une fois les cartes rastérisées, le thread compositeur collecte des informations sur les cartes, appelées quads de dessin, pour créer un frame compositeur.
Dessiner des quads | Contient des informations telles que l'emplacement en mémoire de la carte et l'emplacement sur la page pour dessiner la carte en tenant compte de la composition de la page. |
Frame du compositeur | Ensemble de quads de dessin représentant un frame d'une page. |
Une trame compositeur est ensuite envoyée au processus du navigateur via IPC. À ce stade, un autre frame compositeur peut être ajouté à partir du thread UI pour la modification de l'UI du navigateur ou à partir d'autres processus de moteur de rendu pour les extensions. Ces trames compositeurs sont envoyées au GPU pour les afficher sur un écran. Si un événement de défilement arrive, le thread compositeur crée un autre frame compositeur à envoyer au GPU.
L'avantage de la composition est qu'elle s'effectue sans impliquer le thread principal. Le thread compositeur n'a pas besoin d'attendre le calcul du style ou l'exécution JavaScript. C'est pourquoi la composition uniquement des animations est considérée comme la méthode optimale pour obtenir des performances fluides. Si la mise en page ou la peinture doivent être calculées à nouveau, le thread principal doit être impliqué.
Conclusion
Dans cet article, nous avons abordé le pipeline de rendu, de l'analyse à la composition. Nous espérons que vous êtes maintenant autorisé à en savoir plus sur l'optimisation des performances d'un site Web.
Dans le prochain et le dernier article de cette série, nous examinerons plus en détail le thread du compositeur et verrons ce qui se passe lorsque des entrées utilisateur telles que mouse move
et click
arrivent.
Avez-vous apprécié ce post ? Si vous avez des questions ou des suggestions pour le prochain post, n'hésitez pas à me contacter dans la section des commentaires ci-dessous ou à l'adresse @kosamari sur Twitter.