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.
De l'URL aux balises HTML
-
Vérification de la politique HSTS :
- via une liste hardcodée (fichier
json
de plus de7M
dans Chromium) - et (parfois) une liste des sites visités :
- Chrome :
chrome://net-internals/#hsts
- Safari :
plutil -convert xml1 ~/Library/Cookies/HSTS.plist -o -
- Chrome :
- via une liste hardcodée (fichier
-
Vérification de la présence d'un Service Worker (Editor's draft)
- voir Service Worker Cookbook pour les usages
-
Vérification du cache du navigateur
- contrôlable via les en-têtes HTTP
Cache-Control
dont :Cache-Control: no-store
: pas de cacheCache-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
ouIf-None-Match
- contrôlable via les en-têtes HTTP
-
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 leContent-Type
ou pas si présence de l'en-têteX-Content-Type-Options: nosniff
-
Vérification de sécurité
- sécurité basée sur l'origine : triplet protocole/nom de domaine/port
- Chrome : vérification SafeBrowsing
- Chrome : vérification Cross Origin Read Blocking
-
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
Des balises HTML au DOM
-
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
- vérification de l'en-tête
-
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
- le parseur déclenche l'événement
-
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 !
Du DOM aux pixels
-
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
- depuis des fichiers
-
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.)
- avec
-
Évaluation de la géométrie, de la taille et des coordonnées des boîtes
- en fonction de leurs contextes de formatage (formatting context)
- exemple pour le contexte de formatage block : Block Layout Deep Dive
-
Calcul des valeurs des propriétés des éléments du DOM en fonction du type de media :
- en plusieurs étapes
- évaluation de la valeur de
auto
- 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
- 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
-
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
JavaScript et gestion des événements
-
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
- If we implemented the algorithms exactly as they are described in the specification, we'd end up with a very slow interpreter
- Optimizing Prototypes
- Shapes and Inline Caches
-
Ajout potentiel de contenu en plein milieu de l'analyse HTML via
<script>
oudocument.write
- peut bloquer l'analyse HTML pendant le temps de l'évaluation
- il est possible d'indiquer comment évaluer les
<script>
s avecasync
,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 prochainrequestAnimationFrame
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
)
La mise à jour du rendu est coûteuse
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.