Analyse approfondie de RenderingNG: fragmentation des blocs LayoutNG

La fragmentation des blocs dans LayoutNG est maintenant terminée. Découvrez dans cet article comment elle fonctionne et pourquoi elle est importante.

Morten Stenshorne
Morten Stenshorne

Je m'appelle Morten Stenshorne, ingénieur de mise en page dans l'équipe chargée du rendu Blink chez Google. Je travaille dans le développement de moteurs de navigateur depuis le début des années 2000 et je me suis beaucoup amusé. Par exemple, j'ai réussi à faire passer le test acid2 dans le moteur Presto (Opera 12 et versions antérieures) et à procéder à la rétro-ingénierie d'autres navigateurs pour corriger la disposition des tables dans Presto. J'ai également passé plus d'années que je ne souhaiterais l'admettre sur la fragmentation de blocs, et en particulier sur multicol dans Presto, WebKit et Blink. Au cours des dernières années, je me suis principalement concentrée sur la prise en charge de la fragmentation des blocs dans LayoutNG. Découvrez en détail l'implémentation de la fragmentation de blocs, car c'est peut-être la dernière fois que j'implémente la fragmentation de blocs. :)

Qu'est-ce que la fragmentation de blocs ?

La fragmentation en bloc consiste à diviser une zone CSS au niveau du bloc (comme une section ou un paragraphe) en plusieurs fragments lorsqu'elle ne tient pas dans son ensemble dans un conteneur de fragment appelé fragmentainer. Un fragmentaire n'est pas un élément, mais représente une colonne dans une mise en page multicolonne ou une page dans un support paginé. Pour que la fragmentation se produise, le contenu doit se trouver dans un contexte de fragmentation. Le contexte de fragmentation est généralement établi par un conteneur à plusieurs colonnes (le contenu sera divisé en colonnes) ou lors de l'impression (le contenu sera divisé en pages). Il peut être nécessaire de diviser un paragraphe long comportant de nombreuses lignes en plusieurs fragments, de sorte que les premières lignes soient placées dans le premier fragment et que les lignes restantes soient placées dans les fragments suivants.

Paragraphe de texte divisé en deux colonnes.
Dans cet exemple, un paragraphe a été divisé en deux colonnes utilisant une mise en page à plusieurs colonnes. Chaque colonne est un fragmentaire, représentant un fragment du flux fragmenté.

La fragmentation de blocs est analogue à un autre type bien connu de fragmentation: la fragmentation de lignes (également appelée "saut de ligne"). Tout élément intégré qui se compose de plusieurs mots (nœud de texte, élément <a>, etc.) et qui autorise les sauts de ligne peut être divisé en plusieurs fragments. Chaque fragment est placé dans une zone de ligne différente. Une zone de ligne est l'équivalent de la fragmentation intégrée à un fragmenteur pour les colonnes et les pages.

Qu'est-ce que la fragmentation de blocs LayoutNG ?

LayoutNGBlockFragmentation est une réécriture du moteur de fragmentation pour LayoutNG. Après de nombreuses années de travail, les premières parties ont finalement été publiées dans Chrome 102 plus tôt cette année. Cela a permis de résoudre des problèmes de longue date qui, pour l'essentiel, ne pouvaient pas être résolus dans notre "ancien" moteur. En termes de structures de données, il remplace plusieurs structures de données pré-NG par des fragments NG représentés directement dans l'arborescence de fragments.

Par exemple, nous acceptons désormais la valeur "avoid" pour les propriétés CSS "break-before" et "break-after", qui permet aux auteurs d'éviter les coupures juste après un en-tête. En général, la dernière chose placée sur une page est un en-tête, alors que le contenu de la section commence à la page suivante. Il est préférable de faire un saut de ligne avant l'en-tête. L'image ci-dessous présente un exemple.

Le premier exemple affiche un titre au bas de la page, le second l&#39;affiche en haut de la page suivante avec son contenu associé.

Chrome 102 est également compatible avec le débordement de fragmentation. Ainsi, le contenu monolithique (qui est censé être insensible) n'est pas décomposé en plusieurs colonnes, et les effets de peinture tels que les ombres et les transformations sont appliqués correctement.

La fragmentation des blocs dans LayoutNG est maintenant terminée

Au moment de la rédaction de ce document, nous avons pris en charge la fragmentation de blocs complète dans LayoutNG. Fragmentation principale (conteneurs de blocs, y compris la mise en page des lignes, les flottants et le positionnement hors du flux) disponible dans Chrome 102. La flexibilité et la fragmentation de la grille sont disponibles dans Chrome 103 et la fragmentation des tables dans Chrome 106. Enfin, l'impression a été expédiée dans Chrome 108. La fragmentation des blocs était la dernière fonctionnalité qui dépendait de l'ancien moteur pour effectuer la mise en page. Cela signifie qu'à partir de Chrome 108, l'ancien moteur ne sera plus utilisé pour effectuer la mise en page.

En plus de mettre en page le contenu, les structures de données LayoutNG sont compatibles avec la peinture et les tests de positionnement, mais nous nous appuyons toujours sur certaines anciennes structures de données pour les API JavaScript qui lisent les informations de mise en page, telles que offsetLeft et offsetTop.

Toute mise en page avec NG permettra d'implémenter et de déployer de nouvelles fonctionnalités qui n'ont que des implémentations LayoutNG (et sans équivalent d'un ancien moteur), comme les requêtes de conteneur CSS, le positionnement d'ancrage, MathML et la mise en page personnalisée (Houdini). Nous l'avons expédiée un peu à l'avance pour les requêtes de conteneur, en avertissant les développeurs que l'impression n'était pas encore disponible.

Nous avons lancé la première partie de LayoutNG en 2019, qui se composait d'une mise en page standard de conteneur de blocs, d'une mise en page intégrée, de floats et d'un positionnement hors flux, mais n'était pas compatible avec Flex, la grille ou les tables, ni la fragmentation des blocs. Reprenons l'ancien moteur de mise en page pour les configurations flexible, grille, tables et autres éléments impliquant la fragmentation de blocs. Cela était vrai même pour les éléments block, inline, flottant et hors ligne au sein d'un contenu fragmenté. Comme vous pouvez le voir, la mise à niveau d'un moteur de mise en page aussi complexe sur place est une chorégraphie très délicate.

De plus, croyez-le ou non, la majorité des fonctionnalités de base de la mise en page de fragmentation des blocs LayoutNG avaient déjà été implémentées (sous un indicateur) à la mi-2019. Alors, pourquoi l'expédition a-t-elle pris si longtemps ? Pour faire court, la fragmentation doit coexister correctement avec différentes anciennes parties du système, qui ne peut être ni supprimée, ni mise à niveau tant que toutes les dépendances n'ont pas été mises à niveau. Pour obtenir la réponse longue, consultez les informations suivantes.

Interaction avec l'ancien moteur

Les anciennes structures de données sont toujours en charge des API JavaScript qui lisent les informations de mise en page. Nous devons donc écrire les données à l'ancien moteur de manière à ce qu'il les comprenne. Cela inclut la mise à jour correcte des anciennes structures de données à plusieurs colonnes, telles que LayoutMultiColumnFlowThread.

Détection et gestion des solutions de secours à l'aide d'un ancien moteur

Nous devions revenir à l'ancien moteur de mise en page lorsque du contenu comportait du contenu qui ne pouvait pas encore être géré par la fragmentation de blocs LayoutNG. Au moment de la mise en ligne de la principale fragmentation des blocs LayoutNG (printemps 2022), incluant les éléments Flex, la grille, les tables et tous les éléments imprimés. Cette situation était particulièrement délicate, car nous devions détecter la nécessité d'anciennes créations de remplacement avant de créer des objets dans l'arborescence de mise en page. Par exemple, nous devions détecter avant de savoir s'il existait un ancêtre de conteneur à plusieurs colonnes, et avant de savoir quels nœuds DOM deviendront ou non un contexte de mise en forme. Il s'agit d'un problème de type poulet et œuf qui n'a pas de solution parfaite, mais tant que son seul comportement incorrect est de faux positifs (revenir à l'ancien comportement quand cela n'est pas nécessaire), tout va bien, car tout bug dans ce comportement de mise en page correspond à celui de Chromium, et non à de nouveaux.

Promenade dans les arbres avant de peindre

Le pré-peindre est une opération que nous effectuons après la mise en page, mais avant de peindre. Le principal défi est que nous devons encore parcourir l'arborescence des objets de mise en page, mais nous avons maintenant des fragments NG. Comment gérer cela ? Nous parcourons à la fois l'objet de mise en page et les arborescences de fragments NG. C'est assez compliqué, car le mappage entre les deux arbres n'est pas simple. Bien que l'arborescence des objets de mise en page ressemble fortement à celle de l'arborescence DOM, l'arborescence des fragments est un sortie de mise en page et non une entrée. En plus de refléter réellement l'effet de toute fragmentation, y compris la fragmentation intégrée (fragments de ligne) et la fragmentation de blocs (fragments de colonne ou de page), l'arborescence de fragments présente également une relation parent-enfant directe entre un bloc conteneur et les descendants DOM ayant ce fragment comme bloc conteneur. Par exemple, dans l'arborescence des fragments, un fragment généré par un élément positionné absolument est un enfant direct de son fragment de bloc conteneur, même s'il existe d'autres nœuds dans la chaîne d'ascendance entre le descendant positionné hors du flux et le bloc qui le contient.

Cela se complique encore plus lorsqu'un élément positionné hors du flux se trouve dans la fragmentation, car les fragments hors flux deviennent alors des enfants directs du fragmentainer (et non un enfant de ce que le CSS considère comme le bloc conteneur). Malheureusement, ce problème a dû être résolu pour pouvoir coexister avec l'ancien moteur sans trop d'efforts. À l'avenir, nous devrions être en mesure de simplifier une grande partie de ce code, car LayoutNG est conçu pour prendre en charge de manière flexible tous les modes de mise en page modernes.

Problèmes liés à l'ancien moteur de fragmentation

L'ancien moteur, conçu à une époque du Web, n'a pas vraiment de concept de fragmentation, même si cette dernière existait techniquement à cette époque (pour permettre l'impression). La prise en charge de la fragmentation était simplement quelque chose qui était boulonné (impression) ou réaménagé (multicolonne).

Lors de la mise en page d'un contenu fragmentable, l'ancien moteur présente tout le contenu dans une grande bande dont la largeur correspond à celle d'une colonne ou d'une page, et dont la hauteur est égale à celle nécessaire pour contenir le contenu. Cette grande bande n'est pas affichée sur la page. On peut le voir comme une page virtuelle qui est ensuite réorganisée pour l'affichage final. Le concept est similaire à l'impression d'un article de journal papier entier en une colonne, puis à l'utilisation de ciseaux pour le découper en plusieurs comme deuxième étape. (À l'époque, certains journaux utilisaient des techniques similaires !)

L'ancien moteur effectue le suivi d'une limite de page ou de colonne imaginaire dans la bande. Cela lui permet de déplacer le contenu qui dépasse les limites vers la page ou la colonne suivante. Par exemple, si seule la moitié supérieure d'une ligne peut tenir sur ce que le moteur considère comme étant la page actuelle, il insère un "pas de pagination" pour le pousser à l'endroit où le moteur suppose que le haut de la page suivante se trouve. Ensuite, la majeure partie du travail de fragmentation (le "découpage avec des ciseaux et le positionnement") a lieu après la mise en page lors du pré-peinture et de la peinture, ou en traduisant le contenu en découpant les pages et en le traduisant en découpant les pages et en les rognant sur des colonnes. Cela rendait certaines choses impossibles à comprendre, telles que l'application de transformations et le positionnement relatif après la fragmentation (ce qui est requis par la spécification). De plus, bien que l'ancien moteur prenne en charge la fragmentation des tables, les flexibilités et les grilles ne sont pas du tout prises en charge.

Voici une illustration de la façon dont une mise en page en trois colonnes est représentée en interne dans l'ancien moteur, avant d'utiliser des ciseaux, le placement et la colle (nous avons une hauteur spécifiée, de sorte que seules quatre lignes puissent rentrer, mais il y a un excès d'espace en bas):

La représentation interne se présente sous la forme d&#39;une colonne avec des marges de pagination à l&#39;endroit où le contenu se rompt, et la représentation à l&#39;écran sous la forme de trois colonnes.

Étant donné que l'ancien moteur de mise en page ne fragmente pas le contenu lors de la mise en page, il existe de nombreux artefacts étranges, tels que le positionnement relatif et les transformations appliquées de manière incorrecte, et les "box-shadows" rognés sur les bords des colonnes.

Voici un exemple simple avec text-shadow:

L'ancien moteur ne gère pas cela correctement:

Ombres de texte rognées placées dans la deuxième colonne.

Voyez-vous comment l'ombre du texte de la ligne de la première colonne est rognée et placée en haut de la deuxième colonne ? En effet, l'ancien moteur de mise en page ne comprend pas la fragmentation.

Cela devrait se présenter comme suit (et voici à quoi cela ressemble avec NG):

Deux colonnes de texte avec les ombres affichées correctement.

À présent, ajoutons des transformations et des ombres de type "box-shadow" à la situation un peu plus complexe. Notez que dans l'ancien moteur, le rognage et le contenu des colonnes sont incorrects. En effet, selon les spécifications, les transformations sont censées être appliquées en tant qu'effet post-mise en page et post-fragmentation. Avec LayoutNG, la fragmentation fonctionne correctement. Cela augmente l'interopérabilité avec Firefox, qui prend en charge la fragmentation pendant un certain temps, et la plupart des tests dans ce domaine réussissent également cette tâche.

Des cases sont incorrectement divisées en deux colonnes.

L'ancien moteur rencontre également des problèmes avec le contenu monolithique de grande taille. Le contenu est monolithique s'il ne peut pas être divisé en plusieurs fragments. Les éléments avec défilement par dépassement sont monolithiques, car il est incompréhensible pour les utilisateurs de faire défiler une zone non rectangulaire. Les cadres de ligne et les images sont d'autres exemples de contenu monolithique. Exemple :

Si le contenu monolithique est trop grand pour tenir dans une colonne, l'ancien moteur le décomposera brutalement, ce qui entraînera un comportement très "intéressant" lors du défilement du conteneur à faire défiler :

Plutôt que de la laisser dépasser la première colonne (comme c'est le cas avec la fragmentation de blocs LayoutNG):

ALT_TEXT_HERE

L'ancien moteur accepte les pauses forcées. Par exemple, <div style="break-before:page;"> insère un saut de page avant le DIV. Toutefois, sa prise en charge est limitée pour identifier les sauts de page non forcés optimaux. Il accepte break-inside:avoid, ainsi que les orphelins et veuves, mais il n'est pas possible d'éviter les pauses entre les blocs, si vous en faites la demande via break-before:avoid, par exemple. Prenons l'exemple suivant :

Texte divisé en deux colonnes.

Ici, l'élément #multicol peut contenir cinq lignes dans chaque colonne (car il fait 100 pixels de haut et la hauteur de ligne de 20 pixels). Ainsi, tous les #firstchild peuvent tenir dans la première colonne. Cependant, son frère et la sœur #secondchild ont "break-before:avoid", ce qui signifie que le contenu souhaite qu'aucune pause ne se produise entre eux. Étant donné que la valeur de widows est 2, nous devons insérer deux lignes de #firstchild dans la deuxième colonne pour répondre à toutes les demandes d'évitement des coupures. Chromium est le premier moteur de navigateur à être entièrement compatible avec cette combinaison de fonctionnalités.

Fonctionnement de la fragmentation NG

Le moteur de mise en page NG met généralement en page le document en balayant d'abord la profondeur de l'arborescence de zone CSS. Lorsque tous les descendants d'un nœud sont disposés, la mise en page de ce nœud peut être terminée, en produisant un NGPhysicalFragment et en revenant à l'algorithme de mise en page parent. Cet algorithme ajoute le fragment à sa liste de fragments enfants et, une fois tous les enfants terminés, génère un fragment pour lui-même avec tous ses fragments enfants. Cette méthode crée une arborescence de fragments pour l'ensemble du document. Il s'agit toutefois d'une simplification excessive: par exemple, les éléments positionnés hors du flux devront s'afficher de là où ils existent dans l'arborescence DOM vers leur bloc conteneur avant de pouvoir être disposés. Par souci de simplicité, j'ignore ici ce détail avancé.

Outre la zone CSS elle-même, LayoutNG fournit un espace de contrainte à un algorithme de mise en page. Cela fournit à l'algorithme des informations telles que l'espace disponible pour la mise en page, l'établissement d'un nouveau contexte de mise en forme et les résultats de réduction de la marge intermédiaire provenant du contenu précédent. L'espace de contrainte connaît également la taille de bloc disposée du fragmentainer et le décalage de bloc actuel dans celui-ci. Cela indique où rompre.

En cas de fragmentation de blocs, la disposition des descendants doit s'arrêter à une pause. Il peut s'agir, par exemple, d'un espace insuffisant dans la page ou la colonne, ou d'une coupure forcée. Nous produisons ensuite des fragments pour les nœuds que nous avons visités et revenons jusqu'à la racine du contexte de fragmentation (le conteneur multicol ou, dans le cas d'une impression, la racine du document). Ensuite, à la racine du contexte de fragmentation, nous nous préparons à un nouveau fragmentaire, puis nous descendons à nouveau dans l'arbre, en reprenant là où nous nous sommes arrêtés avant la pause.

La structure de données essentielle pour permettre la reprise de la mise en page après une coupure s'appelle NGBlockBreakToken. Il contient toutes les informations nécessaires pour reprendre correctement la mise en page dans le prochain fragment. Un NGBlockBreakToken est associé à un nœud et constitue une arborescence NGBlockBreakToken de sorte que chaque nœud devant être réactivé est représenté. Un NGBlockBreakToken est associé au NGPhysicalBoxFragment généré pour les nœuds qui s'introduisent dans un fragment. Les jetons de rupture sont propagés aux parents, formant une arborescence de jetons de rupture. Si nous devons rompre avant un nœud (plutôt que de l'intérieur), aucun fragment ne sera généré. Toutefois, le nœud parent doit toujours créer un jeton de rupture de type "pause avant" pour le nœud, afin que nous puissions commencer le déploiement lorsque nous arrivons à la même position dans l'arborescence de nœuds dans le fragment suivant.

Des sauts sont insérés lorsque nous manquons d'espace fragmentaire (une rupture non forcée) ou lorsqu'une coupure forcée est demandée.

La spécification contient des règles permettant d'optimiser les coupures non forcées. Il n'est pas toujours conseillé d'insérer une coupure à l'endroit où l'espace est insuffisant. Par exemple, plusieurs propriétés CSS comme break-before influencent le choix de l'emplacement de la coupure. Par conséquent, lors de la mise en page, afin d'implémenter correctement la section de spécification des raccourcis non forcés, nous devons assurer le suivi des points d'arrêt qui peuvent être de bonne qualité. Cet enregistrement signifie que nous pouvons revenir en arrière et utiliser le dernier point d'arrêt possible trouvé, si nous manquons d'espace à un moment où nous violerons les demandes d'évitement des pannes (par exemple, break-before:avoid ou orphans:7). Chaque point d'arrêt possible se voit attribuer un score, allant de "Ne le faire qu'en dernier recours" à "endroit idéal pour la rupture", avec quelques valeurs entre les deux. Lorsqu'une zone de pause est considérée comme "parfaite", cela signifie qu'aucune règle ne sera enfreinte. Par ailleurs, si nous obtenons ce score au moment précis où l'espace de stockage devient insuffisant, il n'est pas nécessaire de revenir en arrière pour trouver une meilleure solution. Si le score est "dernier recours", le point d'arrêt n'est même pas valide, mais nous pouvons tout de même y rompre si nous ne trouvons rien de mieux, pour éviter un dépassement de fragment.

Les points d'arrêt valides ne se produisent généralement qu'entre des frères (cases de ligne ou blocs) et non, par exemple, entre un parent et son premier enfant (les points d'arrêt de classe C sont une exception, mais nous n'avons pas besoin d'en parler ici). Il existe un point d'arrêt valide, par exemple avant un frère ou sœur de bloc avec "break-before:avoid", mais il se situe quelque part entre "parfait" et "dernier recours".

Pendant la mise en page, nous effectuons le suivi du meilleur point d'arrêt trouvé jusqu'à présent dans une structure appelée NGEarlyBreak. Une coupure précoce est un point d'arrêt possible avant ou à l'intérieur d'un nœud de bloc, ou avant une ligne (une ligne de conteneur de blocs ou une ligne flexible). Nous pouvons former une chaîne ou un chemin d'objets NGEarlyBreak, si le meilleur point d'arrêt se trouve quelque part au fond d'un élément que nous avons parcouru plus tôt, alors que nous manquons d'espace. Exemple :

Dans le cas présent, l'espace est saturé juste avant #second, mais le paramètre "break-before:avoid" est spécifié, ce qui obtient le score d'emplacement de la coupure "Veiller à éviter les pauses". À ce stade, nous avons une chaîne NGEarlyBreak de "dans #outer > dans #middle > à l'intérieur de #inner > avant"ligne 3", avec "perfect". Nous préférons donc le casser. Nous devons donc renvoyer et réexécuter la mise en page à partir du début de #outer (et cette fois, transmettre le NGEarlyBreak que nous avons trouvé), afin de pouvoir rompre avant "ligne 3" dans #inner. (Nous interrompons le code avant la ligne 3 pour que les quatre lignes restantes se retrouvent dans le fragment suivant, ce qui permet de respecter widows:4.)

L'algorithme est conçu pour toujours rompre au meilleur point d'arrêt possible (tel que défini dans la spec) en éliminant les règles dans le bon ordre, si elles ne peuvent pas toutes être satisfaites. Notez qu'il suffit de procéder à une nouvelle mise en page une fois par flux de fragmentation au maximum. Au moment de la deuxième étape de mise en page, le meilleur emplacement de coupure publicitaire a déjà été transmis aux algorithmes de mise en page. Il s'agit de l'emplacement de coupure qui a été découvert lors de la première étape de mise en page et fourni dans la sortie de mise en page de cette série. Lors du deuxième passage de mise en page, nous ne nous exécutons pas tant que l'espace n'est pas saturé. En fait, nous ne sommes pas censés manquer d'espace (ce serait une erreur), car nous avons trouvé un endroit super doux (et aussi agréable que possible) pour insérer une pause anticipée afin d'éviter d'enfreindre inutilement les règles. Nous allons donc avancer jusqu'à ce point et faire une pause.

Par conséquent, nous devons parfois enfreindre certaines demandes de prévention des ruptures, si cela permet d'éviter un dépassement fragmenté. Exemple :

Ici, l'espace est saturé juste avant #second, mais le paramètre "break-before:avoid" est spécifié. Cela se traduit par « enfreindre les pauses – éviter », comme dans le dernier exemple. Nous avons également un NGEarlyBreak avec "enfreignant les orphelins et les veuves" (dans #first > avant "ligne 2"), ce qui n'est toujours pas parfait, mais mieux que "éviter les pauses". Nous allons donc faire un saut avant "ligne 2 ", ce qui va à l'encontre de la demande concernant les orphelins / veuves. La spécification traite ce sujet dans la version 4.4. Les pauses non forcées, qui définissent les règles de rupture à ignorer en premier si le nombre de points d'arrêt est insuffisant pour éviter un dépassement fragmenté.

Résumé

L'objectif fonctionnel principal du projet de fragmentation de blocs LayoutNG était de fournir une implémentation compatible avec l'architecture LayoutNG de tous les éléments compatibles avec l'ancien moteur, et le moins d'autre possible, à l'exception des corrections de bugs. La principale exception concerne la prise en charge de l'évitement des pannes (break-before:avoid, par exemple), car il s'agit d'un élément essentiel du moteur de fragmentation. Il doit donc être présent dès le début, car l'ajouter ultérieurement entraînerait une autre réécriture.

Maintenant que la fragmentation des blocs LayoutNG est terminée, nous pouvons commencer à ajouter de nouvelles fonctionnalités, telles que la prise en charge de formats de page mixtes lors de l'impression, les zones de marge @page à l'impression, box-decoration-break:clone, etc. Et comme pour LayoutNG en général, nous nous attendons à ce que le taux de bugs et la charge de maintenance du nouveau système soient considérablement plus faibles au fil du temps.

Merci de votre attention,

Remerciements

  • Una Kravets pour sa belle "capture d'écran faite main".
  • Chris Harrelson pour le relire, laisser des commentaires et obtenir des suggestions.
  • Philip Jägenstedt pour obtenir des commentaires et des suggestions.
  • Rachel Andrew pour la modification et la première figure d'exemple à plusieurs colonnes.