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

Mariko Kosaka

Fonctionnement interne d'un processus de rendu

Il s'agit de la troisième partie d'une série de quatre articles sur le fonctionnement des navigateurs. Dans les articles précédents, nous avons abordé l'architecture multiprocessus et le flux de navigation. Dans cet article, nous allons examiner ce qui se passe dans le processus de rendu.

Le processus de rendu touche de nombreux aspects des performances Web. Étant donné que de nombreux événements se produisent dans le processus de rendu, cet article n'est qu'une présentation générale. Pour en savoir plus, consultez la section "Performances" de Web Fundamentals.

Les processus de rendu gèrent les contenus Web

Le processus de rendu est responsable de tout ce qui se passe dans un onglet. Dans un processus de rendu, le thread principal gère la plupart du code que vous envoyez à l'utilisateur. Parfois, des parties de votre code JavaScript sont gérées par des threads de travail si vous utilisez un web worker ou un service worker. Les threads de composition et de rastérisation sont également exécutés dans des processus de rendu pour 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 de rendu
Figure 1: Processus de rendu avec un thread principal, des threads de travail, un thread de composition et un thread de rastérisation à l'intérieur

Analyse

Construction d'un DOM

Lorsque le processus 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 d'objet Document Modèle (DOM).

Le DOM est la représentation interne de la page par le 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, une balise </p> fermante manquante 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 aviez écrit 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 élégante. Si vous souhaitez savoir comment ces éléments sont traités, consultez la section Introduction à la gestion des erreurs et aux cas étranges dans l'analyseur de la spécification HTML.

Chargement de sous-ressources

Un site Web utilise généralement des ressources externes telles que des images, du CSS et du JavaScript. Ces fichiers doivent être chargés à partir du réseau ou du cache. Le thread principal pourrait les demander un par un lorsqu'il les trouve lors de l'analyse pour créer un DOM, mais pour accélérer le processus, le "lecteur de préchargement" est exécuté simultanément. Si des éléments tels que <img> ou <link> figurent dans le document HTML, 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 analysant le code HTML et créant un arbre DOM

JavaScript peut bloquer l'analyse

Lorsque l'analyseur HTML détecte une balise <script>, il met en pause 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 (présentation du modèle d'analyse dans la spécification HTML contient un bon diagramme). C'est pourquoi l'analyseur HTML doit attendre l'exécution du code JavaScript avant de pouvoir reprendre l'analyse du document HTML. Si vous souhaitez en savoir plus sur ce qui se passe lors de l'exécution JavaScript, l'équipe V8 propose des conférences et des articles de blog à ce sujet.

Indiquer au navigateur comment vous souhaitez charger les ressources

Les développeurs Web peuvent envoyer des indices au navigateur de plusieurs façons afin de charger les ressources de manière fluide. Si votre code JavaScript n'utilise pas document.write(), vous pouvez ajouter l'attribut async ou defer à la balise <script>. Le navigateur charge et exécute ensuite le code JavaScript de manière asynchrone, sans bloquer l'analyse. Vous pouvez également utiliser un module JavaScript si cela convient. <link rel="preload"> permet d'indiquer au navigateur que la ressource est absolument nécessaire pour la navigation en cours et que vous souhaitez la télécharger dès que possible. Pour en savoir plus, consultez Priorisation des ressources : demander de l'aide au navigateur.

Calcul du style

Un DOM ne suffit pas à savoir à quoi ressemblera la page, car nous pouvons styliser les éléments de la page en CSS. Le thread principal analyse le 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 de DevTools.

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

Même si vous ne fournissez pas de CSS, chaque nœud DOM possède un style calculé. La balise <h1> est affichée 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 souhaitez savoir à quoi ressemble le CSS par défaut de Chrome, consultez le code source ici.

Mise en page

Le processus de rendu connaît désormais 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" n'est pas assez d'informations pour que votre ami sache exactement à quoi ressemblera le tableau.

jeu de machine à fax humaine
Figure 4: 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 contenant des informations telles que les coordonnées x et y et les tailles de la zone de délimitation. L'arborescence de mise en page peut avoir une structure semblable à celle de l'arborescence DOM, mais elle 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 (cependant, un élément avec visibility: hidden est dans l'arborescence de mise en page). De même, si un pseudo-élément avec un contenu tel que p::before{content:"Hi!"} est appliqué, il est inclus dans l'arborescence de mise en page, même s'il ne figure pas dans le DOM.

mise en page
Figure 5: Le thread principal parcourt l'arborescence DOM avec des styles calculés et génère l'arborescence de mise en page
Figure 6: Mise en page en boîte d'un paragraphe déplacé en raison d'un changement de coupure 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 de blocs de haut en bas, doit tenir compte de la taille de la police et de l'emplacement des sauts de ligne, car ils affectent la taille et la forme d'un paragraphe, ce qui affecte ensuite l'emplacement du paragraphe suivant.

Le CSS peut faire flotter un élément d'un côté, masquer un élément en cas de dépassement et modifier les directions d'écriture. Comme vous pouvez l'imaginer, cette étape de mise en page est une tâche de taille. Dans Chrome, une équipe entière d'ingénieurs travaille sur la mise en page. Si vous souhaitez en savoir plus sur leur travail, quelques conférences de la conférence BlinkOn sont enregistrées et très intéressantes à regarder.

Peinture

jeu de dessin
Figure 7: Personne devant un canevas tenant un pinceau, se demandant s'il doit d'abord dessiner un cercle ou un carré

Un DOM, un style et une mise en page ne suffisent toujours pas à 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 déterminer dans quel ordre les peindre.

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

Échec du z-index
Illustration 8: Éléments de page apparaissant dans l'ordre d'un balisage HTML, ce qui entraîne une image affichée incorrecte, car l'indice Z 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, par exemple "arrière-plan d'abord, puis texte, puis rectangle". Si vous avez dessiné sur un élément <canvas> à l'aide de JavaScript, ce processus peut vous sembler familier.

enregistrements de peinture
Figure 9: Le 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: Arbres DOM+Style, Layout et Paint dans l'ordre de génération

L'élément le plus important à 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 de nouvelles données. Par exemple, si quelque chose change dans l'arborescence de mise en page, l'ordre de peinture doit être régénéré pour les parties du document concernées.

Si vous animez des éléments, le navigateur doit exécuter ces opérations entre chaque frame. La plupart de nos écrans se rafraîchissent 60 fois par seconde (60 FPS). L'animation apparaît fluide aux yeux humains lorsque vous déplacez des éléments à l'écran à chaque frame. Toutefois, si l'animation manque les images intermédiaires, la page semblera "défectueuse".

à-coups dus à l&#39;absence de frames
Figure 11: Cadres 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.

jage jank par JavaScript
Illustration 12: Des images d'animation sur une chronologie, mais une image est bloquée par JavaScript

Vous pouvez diviser l'opération JavaScript en petits blocs et planifier son exécution à chaque frame à l'aide de requestAnimationFrame(). Pour en savoir plus, consultez la section Optimiser l'exécution JavaScript. Vous pouvez également exécuter votre JavaScript dans des Web Workers pour éviter de bloquer le thread principal.

frame d&#39;animation demandé
Illustration 13: Morceaux plus petits de code JavaScript exécutés sur une chronologie avec un frame d'animation

Composition

Comment dessiner une page ?

Figure 14: Animation du processus de rastérisation naïve

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

Une solution simple consisterait à rasteriser des parties dans la fenêtre d'affichage. Si un utilisateur fait défiler la page, déplacez le frame rasterisé et remplissez les parties manquantes en effectuant plus de rasterisation. C'est ainsi que Chrome gérait la rastérisation lors de sa première sortie. Toutefois, le navigateur moderne exécute un processus plus sophistiqué appelé "compositing".

Qu'est-ce que le compositing ?

Figure 15: Animation du processus de composition

Le compositing est une technique qui consiste à séparer les parties d'une page en calques, à les échantillonner séparément et à les composer en tant que page dans un thread distinct appelé thread de composition. Si le défilement se produit, étant donné que les calques sont déjà rastérisés, il suffit de composer un nouveau frame. Vous pouvez réaliser une animation de la même manière en déplaçant des calques et en composant un nouveau frame.

Vous pouvez voir comment votre site Web est divisé en calques dans DevTools à l'aide du panneau "Calques".

Diviser en calques

Pour déterminer les éléments qui doivent se trouver 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 de DevTools). Si certaines parties d'une page qui devraient être une couche distincte (comme un menu latéral coulissant) n'en reçoivent pas, vous pouvez en donner un indice au navigateur à l'aide de l'attribut will-change en CSS.

arborescence des calques
Figure 16: Le thread principal parcourant l'arborescence de mise en page pour générer l'arborescence des calques

Vous pourriez être tenté d'ajouter des calques à chaque élément, mais la composition sur un nombre excessif de calques peut ralentir l'opération par rapport à la rastérisation de petites parties d'une page à chaque frame. 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 au moteur de composition et gérer le nombre de calques.

Réseau raster et composite en dehors du thread principal

Une fois l'arborescence des calques créée et les ordres de peinture déterminés, le thread principal valide ces informations auprès du thread du compositeur. Le thread du moteur de rendu effectue ensuite la rastérisation de chaque couche. Une couche peut être aussi grande que la longueur totale d'une page. Le thread du compositeur les divise donc en tuiles et envoie chaque tuile aux threads de rastérisation. Les threads de rastérisation rasterisent chaque tuile et les stockent dans la mémoire du GPU.

trame
Figure 17: Threads de rendu qui créent le bitmap des tuiles et l'envoient au GPU

Le thread du compositeur peut hiérarchiser différents threads de rastérisation afin que les éléments situés dans le viewport (ou à proximité) puissent être rastérisés en premier. Une couche comporte également plusieurs cartes pour différentes résolutions afin de gérer des actions telles que le zoom avant.

Une fois les tuiles rastérisées, le thread du moteur de rendu rassemble les informations sur les tuiles appelées quads de dessin pour créer un cadre de moteur de rendu.

Dessiner des quads Contient des informations telles que l'emplacement de la carte en mémoire et l'emplacement de la page où dessiner la carte en tenant compte de la composition de la page.
Cadre du moteur de composition Ensemble de quads de dessin représentant un cadre de page.

Un frame de composition est ensuite envoyé au processus du navigateur via IPC. À ce stade, un autre frame de compositeur peut être ajouté à partir du thread d'UI pour le changement d'UI du navigateur ou à partir d'autres processus de rendu pour les extensions. Ces frames de composition sont envoyés au GPU pour être affichés à l'écran. Si un événement de défilement est reçu, le thread du moteur de composition crée un autre frame de moteur de composition à envoyer au GPU.

composit
Figure 18: Thread du moteur de composition 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 est effectuée sans impliquer le thread principal. Le thread du moteur de rendu n'a pas besoin d'attendre le calcul des styles ni l'exécution JavaScript. C'est pourquoi la composition uniquement d'animations est considérée comme la meilleure option pour des performances fluides. Si la mise en page ou la peinture doit être calculée à 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 pourrez désormais en savoir plus sur l'optimisation des performances d'un site Web.

Dans le prochain et dernier article de cette série, nous examinerons le thread du moteur de rendu plus en détail et verrons ce qui se passe lorsque des entrées utilisateur telles que mouse move et click sont reçues.

Avez-vous aimé cet article ? Si vous avez des questions ou des suggestions pour un prochain post, n'hésitez pas à me contacter dans la section des commentaires ci-dessous ou sur @kosamari sur Twitter.

étape suivante: L'entrée est transmise au compositeur