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 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.

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 de compositeur et un thread de trame.

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.

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 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.

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

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.

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 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.

mise en page
Figure 5: Le thread principal survolant l'arborescence DOM avec des styles calculés et génère une arborescence de mise en page
Figure 6: mise en page en boîtes pour un paragraphe en mouvement en raison d'un 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 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

jeu de dessin
Figure 7: Une personne se tenant devant une toile et se demandant si elle doit d'abord dessiner un cercle ou un carré

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.

Échec du z-index
Figure 8: Les éléments de la page s'affichent dans l'ordre d'un balisage HTML, ce qui entraîne le rendu d'une image incorrecte, 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é un élément <canvas> à l'aide de JavaScript, ce processus vous est peut-être familier.

registres de peinture
Figure 9: Thread principal parcourant l'arborescence de mise en page et générant des enregistrements de peinture

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

Figure 10: Arborescences de style DOM, de style et de peinture

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".

à-coups dans la jage par des images manquantes
Figure 11: Images d'animation sur une timeline

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.

à-coups dans la jauge par JavaScript
Figure 12: Images d'animation sur une timeline, mais une image est bloquée par 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.

demander un frame d&#39;animation
Figure 13: Extraits de code JavaScript plus petits s'exécutant sur une timeline avec un frame d'animation

Composition

Comment dessineriez-vous une page ?

Figure 14: Animation d'un processus de trame 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 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

Figure 15: Animation du processus de 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.

arborescence des calques
Figure 16: Le thread principal qui traverse l'arborescence de mise en page et génère une arborescence de calques

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.

trame
Figure 17: Threads matriciels qui créent le bitmap des tuiles et les envoient au 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.

composite
Figure 18: Thread compositeur créant un frame de composition. Le frame est envoyé au processus du navigateur, puis 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.

Étape suivante: L'entrée arrive dans le compositeur