Précédent Rétrospective pour les 30 ans du Web
Suivant Héritage contre composition

Comment fonctionne un navigateur ?

Moins de deux mois avant l'annonce par Microsoft de l'abandon d'EdgeHTML, A List Apart publiait une série d'article sur le fonctionnement d'un navigateur rédigés par des membres de l'équipe Microsoft Edge avec une introduction d'Aaron Gustafson :

If you think about it, our whole industry depends on our faith in a handful of “black boxes” few of us fully understand: browsers.

Un tout petit peu avant, Mariko Kosaka publiait une autre série d'articles un peu dans le même esprit mais avec une emphase sur l'architecture de Chrome et la milestone Site Isolation.

Il y a presque 10 ans, un article du même acabit centré sur WebKit (avant le fork) et Gecko était publié sur HTML5 Rocks : Behind the scenes of modern web browsers.

À partir de ces différentes lectures, je vous propose un résumé à grosses mailles de ce qui se passe dans un navigateur quand on affiche une page.

  • Vérification de la politique HSTS :
    • via une liste hardcodée (fichier json de plus de 7M dans Chromium)
    • et (parfois) une liste des sites visités :
      • Chrome : chrome://net-internals/#hsts
      • Safari : plutil -convert xml1 ~/Library/Cookies/HSTS.plist -o -
  • Vérification de la présence d'un Service Worker (Editor's draft)
  • Vérification du cache du navigateur
    • contrôlable via les en-têtes HTTP Cache-Control dont :
      • Cache-Control: no-store : pas de cache
      • Cache-Control: immutable : mettre l'élément en cache indéfiniment
    • si une entrée du cache est disponible mais pas fraîche, le navigateur peut émettre une requête conditionnelle via If-Modified-Since ou If-None-Match
  • La pile réseau du navigateur prend le relai pour établir une connexion
    • requête DNS
    • TLS handshake et vérification du certificat
    • envoi de la requête au serveur
  • Gestion de la réponse
    • vérification des en-têtes de la réponse
    • en cas de redirection, tout le processus recommence
    • gestion par le navigateur des cas de compression et/ou d'envoi segmenté de données (chunked)
    • vérification de l'en-tête Content-Type et MIME type sniffing qui peut surcharger le Content-Type ou pas si présence de l'en-tête X-Content-Type-Options: nosniff
  • Vérification de sécurité
  • Une fois chargée, une page peut continuer à faire des requêtes réseau :
    • ressources de type images, JavaScript, CSS etc.
    • ressources chargées via fetch(), import(), XMLHttpRequest() etc.
    • il est possible de demander au navigateur des ressources en avance via les Resource Hints (Working Draft) :
      • dns-prefetch
      • preconnect
      • prefetch
      • prerender
  • Détermination de l'encodage des caractères :
    • vérification de l'en-tête Content-Type
    • tentative de recherche d'un BOM (byte order mark)
    • tentative d'approximation par heuristique
    • vérification de la balise <meta> du document lui-même
  • Pré-analyse syntaxique :
    • détection des ressources supplémentaires pour réduire au minimum les temps de latence supplémentaires
    • détection des directives preload, prefetch etc.
    • Chrome parle de preload scanner lancé en simultané de l'analyse lexicale
  • Analyse lexicale (Tokenization) :
    • extraction de tokens depuis un balisage HTML propre ou depuis une soupe de tags
    • extraction en mode hardcore si présence du type MIME application/xhtml+xml
    • note : une fois le DOM créé, JavaScript peut le modifier n'importe comment (ajouter une cellule de tableau en tant qu'enfant d'une balise <video> etc.)
  • Construction de l'arbre à partir des tokens de l'étape précédente
    • création des éléments du DOM
    • insertion des éléments dans la structure en arbre du DOM
  • À la fin de l'analyse :
    • le parseur déclenche l'événement DOMContentLoaded
    • n'importe quel changement dans l'arbre du DOM déclenche une réaction en chaîne pour analyser le changement et faire les modifications
  • En plus du DOM les navigateurs proposent un tas d'autres technologies sous forme de langages ou d'APIs dont :
    • SVG
    • MathML
    • CSS

Vous pouvez lire Idiosyncracies of the HTML parser pour avoir plus de détails !

  • Rassemblement du contenu CSS :
    • depuis des fichiers .css externes (<link> ou @import)
    • depuis les balises <style></style> de l'en-tête HTML
    • depuis les attributs style des éléments du DOM
  • Analyse CSS (parsing et tokenization) conformément à la spécification
    • les propriétés raccourcies sont converties en variantes longues
    • le résultat est une structure de donnée comprenant les sélecteurs, les propriétés et leurs valeurs respectives
  • Application des règles de la cascade en fonction du poids des sélecteurs déterminé par :
    • l'origine : utilisateur (vous et moi), auteur (développeur), agent utilisateur (navigateur)
    • la spécificité : les sélecteurs spécifiques prennent le dessus sur les sélecteurs plus généraux (c.f. !important)
    • l'ordre : pour deux sélecteurs de spécificité égale, le gagnant est celui qui apparaît en dernier dans le document
  • Construction de l'arbre des boîtes CSS pour la mise en page (layout)
    • avec display: none, un élément est exclu
    • avec visibility: hidden, un élément est inclus
    • un élément peut être inclus même s'il n'existe pas dans le DOM (pseudo classe ::before etc.)
  • Évaluation de la géométrie, de la taille et des coordonnées des boîtes
  • Calcul des valeurs des propriétés des éléments du DOM en fonction du type de media :
  • Mise à jour du CSS Object Model (CSSOM) permettant de manipuler CSS depuis JavaScript
  • Prise en compte de la fragmentation du contenu le cas échéant (CSS print, CSS Multi-column etc.)
  • Détermination de l'ordre de la mise en peinture
    • pour application des couleurs, des bordures, des ombres etc.
    • un contexte d'empilement (stacking context) créé par z-index change l'ordre de peinture
    • Chrome utilise un processus nommé compositing qui sollicite le GPU pour la mise en peinture
  • Création d'un ou plusieurs calques pour les parties d'une page
    • si certaines parties doivent figurer sur un calque spécifique (à cause d'une animation), on peut l'indiquer au navigateur avec la propriété CSS will-change
  • Rastérisation :
    • chaque élément de la mise en page est converti en une image matricielle (bitmap)
    • les grandes images sont sectionnées en mosaïques (tiles) et stockées dans la mémoire du GPU, ça sert aussi pour le zoom
    • il peut y avoir plusieurs processus de rastérisation priorisés en fonction de leur proximité du viewport
  • Analyse JavaScript :
    • analyse du code
    • construction d'un AST
    • transformation en bytecode
    • passage à l'interpréteur (ou machine virtuelle)
    • compilation à la volée (JIT)
  • Chaque moteur JavaScript a ses techniques d'optimisation
  • Ajout potentiel de contenu en plein milieu de l'analyse HTML via <script> ou document.write
    • peut bloquer l'analyse HTML pendant le temps de l'évaluation
    • il est possible d'indiquer comment évaluer les <script>s avec async, defer, module etc.
  • Création de l'illusion d'interactivité via hit testing
    • un hit test permet de trouver la cible d'un événement
    • pour le navigateur les événements sont tous les gestes de l'utilisateur
    • certains navigateurs regroupent les événements fréquents (mousewheel, mousemove, pointermove, touchmove etc.) et en retardent l'envoi jusqu'avant le prochain requestAnimationFrame pour soulager le processus principal (main thread)
    • les événements moins fréquents (keydown, keyup, mouseup, mousedown, touchstart) sont transmis immédiatement
  • Une région d'une page contenant un gestionnaire d'événement est marquée en tant que Non-Fast Scrollable Region
    • attention : l'event delegation peut facilement marquer l'ensemble de la page comme Non-Fast Scrollable Region
    • il est possible de mitiger cela avec les événements passifs (passive: true)

Une fois que la page est affichée, l'histoire n'est pas terminée puisqu'il est possible de continuer à modifier le DOM n'importe quand.

Une modification du DOM en elle-même n'est pas spécialement lente, mais tout le processus de rendu graphique qui s'en suit est coûteux.

Il existe des listes de ce qui peut déclencher un nouveau processus de rendu graphique, mais elles sont susceptibles de changer au fil du temps en fonction de l'évolution des navigateurs.

Il faut faire attention à regrouper toutes les mutations du DOM et à les appliquer en une seule fois sur le DOM réel, et ça se fait relativement bien via l'interface DocumentFragment du DOM.

Les librairies à la mode (React, Vue.js, Elm etc.) utilisent un motif souvent nommé Virtual DOM qui (entre autres choses) automatise ce processus.