Au cœur du navigateur Web moderne (partie 3)

Mariko Kosaka

Fonctionnement interne d'un processus de moteur de rendu

Il s'agit de la troisième partie de notre série de quatre articles de blog consacrée au fonctionnement des navigateurs. Nous avons précédemment abordé l'architecture multiprocessus et le flux de navigation. Nous allons voir ce qui se passe dans le processus du moteur de rendu.

Le processus du moteur de rendu touche de nombreux aspects des performances Web. Étant donné qu'il se passe beaucoup de choses dans le processus du moteur de rendu, cet article n'est qu'une présentation générale. Si vous souhaitez approfondir vos connaissances, vous trouverez de nombreuses autres ressources dans la section Performances du site Web Fundamentals.

Les processus du moteur de rendu gèrent les contenus Web

Le processus du 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 calcul si vous utilisez un nœud de calcul Web ou un service worker. Les fils de discussion du compositeur et de la trame sont également exécutés à l'intérieur des processus d'un moteur de rendu afin d'afficher une page de manière efficace et fluide.

La tâche principale du processus de rendu consiste à transformer le code HTML, CSS et JavaScript en une page Web avec laquelle l'utilisateur peut interagir.

Processus du moteur de rendu
Figure 1: Processus de moteur de rendu avec un thread principal, des threads de nœud de calcul, un thread compositeur et un thread matriciel à l'intérieur

analyse

Construction d'un DOM

Lorsque le processus du moteur de rendu reçoit un message de validation pour une navigation et commence à recevoir des données HTML, le thread principal commence à analyser la chaîne de texte (HTML) et à la transformer en Modèle Document Object (DOM).

Le DOM est la représentation interne de la page par un navigateur, ainsi que la structure de données et l'API avec lesquelles le développeur Web peut 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 tel que 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, lisez la section An introduction to error management and broken cases in the parser (Présentation de la gestion des erreurs et des cas étranges dans l'analyseur) de la spécification HTML.

Chargement des sous-ressources

Un site Web utilise généralement des ressources externes telles que des images, CSS et JavaScript. Ces fichiers doivent être chargés depuis le réseau ou le cache. Le thread principal pourrait les demander un par un lorsqu'ils les trouvent lors de l'analyse pour créer un DOM, mais pour accélérer, l'outil d'analyse du préchargement est exécuté simultanément. Si le document HTML contient des éléments tels que <img> ou <link>, l'analyseur de préchargement examine les jetons générés par l'analyseur HTML et envoie des requêtes au thread réseau dans le processus du navigateur.

DOM
Figure 2: Le thread principal analyse le code HTML et crée une arborescence DOM

JavaScript peut bloquer l'analyse

Lorsque l'analyseur HTML trouve une balise <script>, il suspend 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'intégralité de la structure DOM (la présentation du modèle d'analyse dans la spécification HTML contient un joli schéma). C'est pourquoi l'analyseur HTML doit attendre que JavaScript s'exécute avant de pouvoir reprendre l'analyse du document HTML. Si vous êtes curieux de savoir ce qui se passe dans l'exécution JavaScript, l'équipe V8 a discuté et publié des articles de blog à ce sujet.

Conseil pour indiquer au navigateur comment vous souhaitez charger les ressources

Il existe de nombreuses façons pour les développeurs Web d'envoyer des indices 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 cela vous convient. <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 la page Hiérarchisation des ressources : Le navigateur pour vous aider.

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. Ces informations indiquent 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.

Style calculé
Figure 3: Le thread principal analyse le code CSS pour ajouter un style calculé

Même si vous ne fournissez aucun code CSS, un style est calculé pour chaque nœud DOM. La balise <h1> s'affiche plus grande que 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 code CSS par défaut de Chrome, cliquez ici pour consulter le code source.

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 par téléphone. « Il y a un grand cercle rouge et un petit carré bleu » ne suffit pas pour que votre ami sache exactement à quoi ressemblerait le tableau.

jeu de fax humain
Figure 4: Une personne debout devant un tableau, ligne téléphonique connectée à l'autre personne

La mise en page est un processus permettant de trouver la géométrie des éléments. Le thread principal parcourt le DOM et les styles calculés, puis crée l'arborescence de mise en page qui contient des informations telles que les coordonnées xy et la taille du cadre de délimitation. L'arborescence de mise en page peut avoir une structure semblable à l'arborescence DOM, mais elle ne contient que des informations liées au contenu 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 un pseudo-élément avec du contenu tel que p::before{content:"Hi!"} est appliqué, il est inclus dans l'arborescence de mise en page même s'il ne se trouve pas dans le DOM.

mise en page
Figure 5: Le thread principal traverse l'arborescence DOM avec des styles calculés et génère l'arborescence de mise en page
Figure 6: Mise en page en cases d'un paragraphe se déplaçant en raison d'un changement de saut de ligne

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 flux en bloc 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.

Le CSS peut faire flotter l'élément d'un côté, masquer l'élément de dépassement et modifier le sens d'écriture. Vous pouvez imaginer que 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 en savoir plus sur leur travail, quelques discussions de la BlinkOn Conference sont enregistrées et sont tout à fait intéressantes à regarder.

Peindre

jeu de dessin
Figure 7: Une personne devant un canevas tenant un pinceau, qui se demande si elle doit d'abord dessiner un cercle ou un carré

Il ne suffit toujours pas d'utiliser un DOM, un style et une mise en page 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 l'ordre dans lequel vous les peignez.

Par exemple, z-index peut être défini pour certains éléments. Dans ce cas, peindre dans l'ordre des éléments écrits dans le code HTML entraînera un rendu incorrect.

échec du z-index
Figure 8: Éléments de la page affichés dans l'ordre d'un balisage HTML, entraînant un rendu incorrect de l'image, car le z-index n'a pas été pris en compte

À 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é sur un élément <canvas> à l'aide de JavaScript, vous connaissez peut-être ce processus.

enregistrements de peinture
Figure 9: Thread principal parcourant l'arborescence de mise en page et produisant des enregistrements de peinture

La mise à jour du pipeline de rendu est coûteuse

Figure 10: Arborescences "DOM+Style", "Layout" et "Paint" dans l'ordre dans lequel elles sont générées

La chose la plus importante à comprendre dans le pipeline de rendu est qu'à chaque étape, le résultat de l'opération précédente est utilisé pour créer des données. Par exemple, si un élément change dans l'arborescence de mise en page, l'ordre d'application doit être régénéré pour les parties concernées du document.

Si vous animez des éléments, le navigateur doit exécuter ces opérations entre chaque frame. La plupart de nos écrans actualisent l'écran 60 fois par seconde (60 FPS). L'animation est fluide aux yeux humains lorsque vous déplacez des éléments sur l'écran à chaque image. Toutefois, si l'animation manque d'images entre les deux, la page apparaîtra comme saccadée.

à-coups par des frames manquants
Figure 11: Images d'animation sur une timeline

Même si vos opérations de rendu suivent le rythme de l'actualisation de l'écran, ces calculs s'exécutent sur le thread principal, ce qui signifie qu'ils peuvent être bloqués lorsque votre application exécute JavaScript.

jage jank par JavaScript
Figure 12: Images d'animation sur une timeline, mais une image est bloquée par JavaScript

Vous pouvez diviser une opération JavaScript en petits fragments et planifier son exécution à 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 nœuds de calcul Web pour éviter de bloquer le thread principal.

demander une image d&#39;animation
Figure 13: De petits segments de JavaScript exécutés sur une timeline avec un frame d'animation

Composition

Comment dessineriez-vous une page ?

Figure 14: Animation du processus de matriçage simpliste

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 des peintures, comment dessine-t-il une page ? La transformation de ces informations en pixels à l'écran s'appelle la rastérisation.

Une façon simple de gérer cela serait peut-être de matricier des parties à l'intérieur de la fenêtre d'affichage. Si un utilisateur fait défiler la page, déplacez le cadre matriciel et remplissez les parties manquantes en effectuant davantage de trame. C'est ainsi que Chrome a géré la rastérisation dès sa sortie. Cependant, le navigateur moderne exécute un processus plus sophistiqué appelé composition.

Qu'est-ce que la composition

Figure 15: Animation du processus de composition

La composition est une technique qui consiste à séparer les parties d'une page en calques, à les rastériser séparément et à les assembler en tant que page dans un fil de discussion distinct appelé "thread compositeur". En cas de défilement, étant donné que les calques sont déjà rastérisés, il ne reste plus qu'à créer une image composite. Vous pouvez obtenir une animation de la même manière en déplaçant des couches et en compilant une nouvelle image.

Vous pouvez voir comment votre site Web est divisé en couches dans les outils de développement à l'aide du panneau des couches.

Division en couches

Pour savoir quels éléments doivent se trouver dans quelles couches, le thread principal parcourt l'arborescence de mise en page pour créer l'arborescence de 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 être des couches distinctes (comme le menu latéral coulissant) n'en reçoivent pas, vous pouvez indiquer au navigateur en utilisant l'attribut will-change dans le CSS.

arborescence de calques
Figure 16: Le thread principal parcourant l'arborescence de mise en page produisant une arborescence de calques

Vous pourriez être tenté de donner des calques à chaque élément, mais la composition sur un nombre excessif de calques peut ralentir le processus que la rastérisation de petites parties d'une page à chaque image. Il est donc essentiel de mesurer les performances de rendu de votre application. Pour en savoir plus, consultez S'en tenir aux propriétés réservées aux compositeurs et gérer le nombre de calques.

Trame et composite hors du thread principal

Une fois l'arborescence de calques 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 être volumineux comme la longueur totale d'une page. Par conséquent, le thread du compositeur les divise en tuiles et envoie chaque tuile vers des fils de trame. Les threads de trame rastérisent chaque carte et les stockent en mémoire GPU.

trame
Figure 17: Fils de trame créant le bitmap des tuiles et l'envoi au GPU

Le thread compositeur peut donner la priorité à différents fils de trame afin que les éléments 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 qu'un zoom avant.

Une fois les tuiles rastérisées, le thread du compositeur collecte des informations de tuile appelées draw quads pour créer un cadre du compositeur.

Dessiner quads Contient des informations telles que l'emplacement de la carte en mémoire et l'emplacement où la carte doit être dessinée sur la page, en tenant compte de la composition de la page.
Cadre compositeur Ensemble de quads de dessin représentant le cadre 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 d'autres processus de rendu pour les extensions. Ces images compositeur sont envoyées au GPU pour être affichées sur un écran. Si un événement de défilement se produit, le thread du compositeur crée un autre frame compositeur à envoyer au GPU.

composition
Figure 18: Thread du compositeur créant le frame de composition Le frame est envoyé au processus du navigateur, puis au GPU

L'avantage de la composition est qu'elle se fait sans impliquer le thread principal. Le thread compositeur n'a pas besoin d'attendre le calcul du style ou l'exécution de JavaScript. C'est pourquoi la composition uniquement des animations est considérée comme la meilleure solution pour 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 examiné le pipeline de rendu, de l'analyse à la composition. Nous espérons que vous avez maintenant toutes les cartes en main pour en savoir plus sur l'optimisation des performances d'un site Web.

Dans le prochain et dernier post de cette série, nous examinerons le thread du compositeur plus en détail 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 les poser dans la section des commentaires ci-dessous ou à écrire à @kosamari sur Twitter.

Suivant: L'entrée est transmise au compositeur