Подробное описание RenderingNG: фрагментация блоков LayoutNG

Фрагментация блоков в LayoutNG завершена. Узнайте, как это работает и почему это важно, в этой статье.

Мортен Стеншорн
Morten Stenshorne

Я Мортен Стеншорн, инженер-верстальщик в команде рендеринга Blink в Google. Я занимаюсь разработкой движка браузера с начала 2000-х годов, и мне было очень весело, например, помогая пройти тест acid2 в движке Presto (Opera 12 и более ранние версии), а также заниматься реверс-инжинирингом других браузеров для исправить макет таблицы в Presto. Я также потратил больше этих лет, чем мне хотелось бы признать, на фрагментацию блоков и, в частности, на multicol в Presto, WebKit и Blink. В течение последних нескольких лет в Google я в основном занимался работой по добавлению поддержки фрагментации блоков в LayoutNG . Присоединяйтесь ко мне в этом глубоком погружении в реализацию фрагментации блоков, поскольку вполне возможно, что это последний раз, когда я реализую фрагментацию блоков. :)

Что такое фрагментация блоков?

Фрагментация блоков — это разделение блока уровня блока CSS (например, раздела или абзаца) на несколько фрагментов, когда он не помещается целиком внутри одного контейнера фрагментов, называемого фрагментаинером . Фрагментатор не является элементом, а представляет собой столбец в многоколоночном макете или страницу в постраничном мультимедиа. Чтобы фрагментация произошла, контент должен находиться внутри контекста фрагментации . Контекст фрагментации чаще всего устанавливается в контейнере с несколькими столбцами (содержимое будет разбито на столбцы) или при печати (содержимое будет разделено на страницы). Длинный абзац со многими строками, возможно, придется разбить на несколько фрагментов, чтобы первые строки помещались в первый фрагмент, а оставшиеся строки — в последующие фрагменты.

Абзац текста, разбитый на две колонки.
В этом примере абзац разделен на два столбца с использованием многоколоночного макета. Каждый столбец представляет собой фрагментайнер, представляющий собой фрагмент фрагментированного потока.

Фрагментация блоков аналогична другому широко известному типу фрагментации: фрагментации строк (также известной как «разрыв строки»). Любой встроенный элемент, состоящий из более чем одного слова (любой текстовый узел, любой элемент <a> и т. д.) и допускающий разрывы строк, может быть разбит на несколько фрагментов. Каждый фрагмент помещается в отдельный линейный блок. Строковый блок — это встроенная фрагментация, эквивалентная фрагментатору для столбцов и страниц.

Что такое фрагментация блоков LayoutNG?

LayoutNGBlockFragmentation — это переписанный механизм фрагментации для LayoutNG, и после многих лет работы первые его части наконец-то появились в Chrome 102 в начале этого года. Это исправило давние проблемы, которые практически невозможно было исправить в нашем «устаревшем» движке. Что касается структур данных, он заменяет несколько структур данных до NG фрагментами NG, представленными непосредственно в дереве фрагментов .

Например, теперь мы поддерживаем значение «avoid» для свойств CSS «break-before» и «break-after» , которые позволяют авторам избегать разрывов сразу после заголовка. Обычно выглядит не очень хорошо, если последнее, что размещается на странице, — это заголовок, а содержимое раздела начинается на следующей странице. Вместо этого лучше сделать перерыв перед заголовком. См. пример на рисунке ниже.

В первом примере показан заголовок внизу страницы, во втором — вверху следующей страницы со связанным содержимым.

Chrome 102 также поддерживает переполнение фрагментации, поэтому монолитное (предполагаемое неразрушимое) содержимое не разбивается на несколько столбцов, а эффекты рисования, такие как тени и преобразования, применяются правильно.

Фрагментация блоков в LayoutNG завершена.

На момент написания этой статьи мы завершили полную поддержку фрагментации блоков в LayoutNG. Фрагментация ядра (блок-контейнеры, включая расположение строк, плавающие элементы и позиционирование вне потока) реализована в Chrome 102. Фрагментация Flex и сетки реализована в Chrome 103, а фрагментация таблиц — в Chrome 106. Наконец, в Chrome 108 реализована печать . Фрагментация блоков была последней функцией, которая зависела от устаревшего движка при выполнении компоновки. Это означает, что начиная с Chrome 108 устаревший движок больше не будет использоваться для макетирования.

Помимо фактического размещения контента, структуры данных LayoutNG поддерживают рисование и проверку попадания, но мы по-прежнему полагаемся на некоторые устаревшие структуры данных для API JavaScript, которые считывают информацию о макете, такие как offsetLeft и offsetTop .

Компоновка всего с помощью NG позволит реализовать и отправить новые функции, которые имеют только реализации LayoutNG (и не имеют аналогов в устаревшем движке), такие как запросы CSS-контейнеров , позиционирование привязки, MathML и пользовательский макет (Houdini) . Для контейнерных запросов мы выпустили его немного заранее, предупредив разработчиков, что печать еще не поддерживается.

Мы выпустили первую часть LayoutNG в 2019 году, которая состояла из обычного макета блочного контейнера, встроенного макета, плавающих элементов и позиционирования вне потока, но без поддержки flex, сетки или таблиц, а также вообще без поддержки фрагментации блоков. Мы бы вернулись к использованию устаревшего механизма компоновки для гибкости, сетки, таблиц и всего, что связано с фрагментацией блоков. Это справедливо даже для блочных, встроенных, плавающих и внепоточных элементов внутри фрагментированного контента — как видите, обновление такого сложного механизма компоновки на месте — очень деликатный танец.

Кроме того, хотите верьте, хотите нет, но к середине 2019 года большая часть основных функций макета фрагментации блоков LayoutNG уже была реализована (за флагом). Итак, почему доставка заняла так много времени? Короткий ответ: фрагментация должна правильно сосуществовать с различными устаревшими частями системы, которые нельзя удалить или обновить, пока не будут обновлены все зависимости. Подробный ответ см. в следующих деталях.

Взаимодействие с устаревшим движком

Устаревшие структуры данных по-прежнему отвечают за API-интерфейсы JavaScript, которые считывают информацию о макете, поэтому нам необходимо записывать данные обратно в устаревший движок так, чтобы он понимал это. Это включает в себя правильное обновление устаревших многостолбцовых структур данных, таких как LayoutMultiColumnFlowThread .

Обнаружение и обработка отказа устаревшего двигателя

Нам пришлось вернуться к устаревшему механизму компоновки, когда внутри был контент, который еще не мог быть обработан фрагментацией блоков LayoutNG. На момент выпуска основной фрагментации блоков LayoutNG (весна 2022 г.), которая включала гибкие элементы, сетку, таблицы и все, что печатается. Это было особенно сложно, потому что нам нужно было обнаружить необходимость использования устаревшего резервного варианта перед созданием объектов в дереве макета . Например, нам нужно было определить, прежде чем мы узнаем, существует ли предок контейнера с несколькими столбцами, и прежде чем мы узнаем, какие узлы DOM станут контекстом форматирования или нет. Это проблема курицы и яйца, у которой нет идеального решения, но пока ее единственным неправильным поведением являются ложные срабатывания (возврат к наследию, когда в этом нет необходимости), это нормально, потому что любые ошибки в этом поведении макета являются ошибками. У Хрома уже есть, а не новых.

Предварительная покраска прогулки по деревьям

Предварительная покраска — это то, что мы делаем после макета , но перед покраской. Основная проблема заключается в том, что нам все еще нужно пройти по дереву объектов макета , но теперь у нас есть NG-фрагменты — так как же нам с этим справиться? Мы проходим одновременно и по объекту макета, и по дереву фрагментов NG! Это довольно сложно, поскольку сопоставление двух деревьев нетривиально. Хотя древовидная структура объектов макета очень похожа на структуру дерева DOM, дерево фрагментов является выходом макета, а не входными данными для него. Помимо фактического отражения эффекта любой фрагментации, включая встроенную фрагментацию (фрагменты строк) и фрагментацию блоков (фрагменты столбцов или страниц), дерево фрагментов также имеет прямую связь родитель-потомок между содержащим блоком и потомками DOM, которые имеют этот фрагмент как содержащий их блок. Например, в дереве фрагментов фрагмент, сгенерированный абсолютно позиционированным элементом, является прямым дочерним элементом содержащего его фрагмента блока, даже если в цепочке предков между позиционированным потомком вне потока и содержащим его блоком есть другие узлы.

Все становится еще сложнее, когда внутри фрагментации находится элемент, позиционированный вне потока, потому что тогда фрагменты вне потока становятся прямыми дочерними элементами фрагментатора (а не дочерними элементами того, что CSS считает содержащим блоком). К сожалению, это была проблема, которую нужно было решить, чтобы без особых проблем сосуществовать с устаревшим движком. В будущем мы сможем упростить большую часть этого кода, поскольку LayoutNG спроектирован так, чтобы гибко поддерживать все современные режимы компоновки.

Проблемы с устаревшим механизмом фрагментации

Устаревший движок, разработанный в более раннюю эпоху Интернета, на самом деле не имеет концепции фрагментации, даже если технически фрагментация существовала и тогда (для поддержки печати). Поддержка фрагментации была просто чем-то, что прикручивалось сверху (печать) или дооснащалось (многоколонка).

При размещении фрагментированного контента устаревший движок раскладывает все в длинную полосу, ширина которой равна строковому размеру столбца или страницы, а высота настолько высока, насколько необходимо для размещения содержимого. Эта длинная полоса не отображается на странице — думайте об этом как о рендеринге на виртуальной странице, которая затем перестраивается для окончательного отображения. Концептуально это похоже на то, как напечатать всю бумажную газетную статью в одну колонку, а затем разрезать ее ножницами на несколько частей в качестве второго шага. (Раньше некоторые газеты использовали подобные методы!)

Устаревший механизм отслеживает воображаемую границу страницы или столбца в полосе. Это позволяет переместить контент, который не выходит за границу, на следующую страницу или столбец. Например, если только верхняя половина строки поместится на то, что движок считает текущей страницей, он вставит «стойку нумерации страниц», чтобы сдвинуть ее вниз до положения, в котором движок предполагает, что верхняя часть следующей страницы находится . Затем большая часть фактической работы по фрагментации («вырезание ножницами и размещение») происходит после макета во время предварительной раскраски и рисования путем разрезания длинной полосы контента на страницы или столбцы (путем обрезки и перевода частей). Это сделало некоторые вещи практически невозможными, например применение преобразований и относительного позиционирования после фрагментации (чего требует спецификация). Более того, хотя в устаревшем движке есть некоторая поддержка фрагментации таблиц, поддержка гибкой фрагментации или фрагментации сетки отсутствует вообще.

Вот иллюстрация того, как трехколоночный макет представляется внутри устаревшего движка до использования ножниц, размещения и склеивания (у нас есть указанная высота, так что помещаются только четыре строки, но внизу остается немного лишнего места):

Внутреннее представление в виде одного столбца со стойками нумерации страниц, где содержимое разбивается, а экранное представление в виде трех столбцов.

Поскольку устаревший механизм компоновки фактически не фрагментирует содержимое во время компоновки, существует множество странных артефактов, таких как относительное позиционирование и неправильное применение преобразований, а также обрезка теней блоков по краям столбцов.

Вот простой пример с text-shadow:

Устаревший движок не справляется с этим хорошо:

Обрезанные тени текста помещены во второй столбец.

Видите ли вы, как тень текста от строки в первом столбце обрезается и вместо этого помещается вверху второго столбца? Это потому, что устаревший механизм компоновки не понимает фрагментацию!

Это должно выглядеть так (и вот как это выглядит с NG):

Два столбца текста с корректным отображением теней.

Далее, давайте немного усложним задачу, используя трансформации и box-shadow. Обратите внимание, что в устаревшем движке происходит неправильное отсечение и вынос столбцов. Это связано с тем, что преобразования по спецификации должны применяться как эффект после компоновки и после фрагментации. При фрагментации LayoutNG оба работают правильно. Это увеличивает взаимодействие с Firefox, который уже некоторое время имеет хорошую поддержку фрагментации, и большинство тестов в этой области также проходят там.

Ящики неправильно разбиты по двум столбцам.

У устаревшего движка также есть проблемы с высоким монолитным контентом. Контент считается монолитным, если он не может быть разбит на несколько фрагментов. Элементы с прокруткой переполнения являются монолитными, поскольку пользователям не имеет смысла прокручивать непрямоугольную область. Линейные блоки и изображения — другие примеры монолитного контента. Вот пример:

Если часть монолитного контента слишком велика, чтобы поместиться внутри столбца, устаревший движок жестоко разрезает ее (что приводит к очень «интересному» поведению при попытке прокрутки прокручиваемого контейнера):

Вместо того, чтобы позволить ему переполнить первый столбец (как это происходит с фрагментацией блока LayoutNG):

ALT_TEXT_ЗДЕСЬ

Устаревший движок поддерживает принудительные перерывы. Например, <div style="break-before:page;"> вставит разрыв страницы перед элементом DIV. Однако он имеет лишь ограниченную поддержку для поиска оптимальных невынужденных перерывов. Он поддерживает break-inside:avoid и Orphans и Widows , но не поддерживает предотвращение разрывов между блоками, если это запрошено, например, через break-before:avoid . Рассмотрим этот пример:

Текст разбит на две колонки.

Здесь элемент #multicol имеет место для 5 строк в каждом столбце (потому что его высота 100 пикселей, а высота строки — 20 пикселей), поэтому весь #firstchild может поместиться в первый столбец. Однако у его родственного элемента #secondchild есть параметр Break-Before:avoid, что означает, что содержимое не желает, чтобы между ними возникал разрыв. Поскольку значение widows равно 2, нам нужно поместить 2 строки #firstchild во второй столбец, чтобы удовлетворить все запросы на предотвращение разрывов. Chromium — первый браузерный движок, полностью поддерживающий такое сочетание функций.

Как работает фрагментация NG

Механизм компоновки NG обычно размещает документ, проходя сначала по глубине дерево блоков CSS. Когда все потомки узла размещены, макет этого узла можно завершить, создав NGPhysicalFragment и вернувшись к алгоритму родительского макета. Этот алгоритм добавляет фрагмент в свой список дочерних фрагментов и, как только все дочерние фрагменты будут завершены, генерирует для себя фрагмент со всеми дочерними фрагментами внутри. С помощью этого метода создается дерево фрагментов для всего документа. Однако это чрезмерное упрощение: например, элементы, позиционированные вне потока, должны будут переместиться из того места, где они существуют в дереве DOM, в содержащий их блок, прежде чем их можно будет разместить. Я игнорирую здесь эту дополнительную деталь ради простоты.

Наряду с самим блоком CSS LayoutNG предоставляет пространство ограничений для алгоритма макета. Это предоставляет алгоритму такую ​​информацию, как доступное пространство для макета, установлен ли новый контекст форматирования, а также результаты схлопывания промежуточных полей из предыдущего содержимого. Пространство ограничений также знает размер блока фрагментайнера и текущее смещение блока в нем. Это указывает, где нужно сломаться.

Когда задействована фрагментация блоков, размещение потомков должно останавливаться на разрыве. Причинами разрыва могут быть нехватка места на странице или столбце, а также принудительный разрыв. Затем мы создаем фрагменты для узлов, которые мы посетили, и возвращаемся вплоть до корня контекста фрагментации (мультикол-контейнер или, в случае печати, корень документа). Затем в корне контекста фрагментации мы готовимся к новому фрагментатору и снова спускаемся в дерево, возобновляя работу с того места, на котором мы остановились перед разрывом.

Важнейшая структура данных, обеспечивающая возможность возобновления макета после перерыва, называется NGBlockBreakToken . Он содержит всю информацию, необходимую для правильного возобновления верстки в следующем фрагментайнере. NGBlockBreakToken связан с узлом и формирует дерево NGBlockBreakToken, так что представлен каждый узел, который необходимо возобновить. NGBlockBreakToken прикрепляется к NGPhysicalBoxFragment , созданному для узлов, которые проникают внутрь. Токены разрыва передаются родителям, образуя дерево токенов разрыва. Если нам нужно разорвать узел перед узлом (а не внутри него), фрагмент не будет создан, но родительскому узлу все равно необходимо создать для узла токен разрыва «до разрыва», чтобы мы могли начать его размещение, когда мы попадаем в ту же позицию в дереве узлов в следующем фрагментайнере.

Разрывы вставляются, когда у нас заканчивается пространство фрагментатора (непринудительный разрыв) или когда запрашивается принудительный разрыв.

В спецификации есть правила для оптимальных непринудительных разрывов, и просто вставлять разрыв именно там, где нам не хватает места, не всегда правильно. Например, существуют различные свойства CSS, такие как break-before , которые влияют на выбор места разрыва. Поэтому во время макета, чтобы правильно реализовать раздел спецификации непринудительных разрывов , нам необходимо отслеживать возможно хорошие точки останова. Эта запись означает, что мы можем вернуться и использовать последнюю найденную наилучшую возможную точку останова, если у нас закончится место в точке, где мы нарушим запросы предотвращения прерывания (например, break-before:avoid или orphans:7 ). Каждой возможной точке останова присваивается оценка в диапазоне от «делайте это только в крайнем случае» до «идеальное место для остановки» с некоторыми промежуточными значениями. Если место разрыва оценивается как «идеальное», это означает, что никакие правила нарушения не будут нарушены, если мы сломаем его (и если мы получим эту оценку точно в тот момент, когда у нас закончится место, нет необходимости оглядываться назад в поисках чего-то лучшего). ). Если оценка является «последней надеждой», точка останова даже не является допустимой, но мы все равно можем прервать ее, если не найдем ничего лучшего, чтобы избежать переполнения фрагментайнера.

Действительные точки останова обычно возникают только между одноуровневыми элементами (строчными блоками или блоками), а не, например, между родительским элементом и его первым дочерним элементом ( точки останова класса C являются исключением, но нам не нужно обсуждать их здесь). Например, перед родственным блоком с помощью Break-Before:avoid существует действующая точка останова, но она находится где-то между «идеальным» и «последним средством».

Во время макета мы отслеживаем лучшую точку останова, найденную на данный момент, в структуре под названием NGEarlyBreak . Ранний разрыв — это возможная точка останова перед или внутри узла блока или перед строкой (либо строкой блочного контейнера, либо гибкой линией). Мы можем сформировать цепочку или путь из объектов NGEarlyBreak на тот случай, если лучшая точка останова находится где-то глубоко внутри того, мимо чего мы прошли ранее, когда у нас закончилось место. Вот пример:

В этом случае нам не хватает места прямо перед #second , но у него есть «break-before:avoid», что дает оценку места разрыва «нарушение предотвращения разрыва». В этот момент у нас есть цепочка NGEarlyBreak «внутри #outer > внутри #middle > внутри #inner > перед «строкой 3» с «идеально», поэтому мы предпочли бы прервать ее. Поэтому нам нужно вернуться и перезапустить макет с начала #outer (и на этот раз передайте найденный нами NGEarlyBreak), чтобы мы могли разбить перед «строкой 3» в #inner (Мы разрываем перед «строкой 3», чтобы оставшиеся 4 строки оказались в конечном итоге. в следующем фрагментайнере, и в честь widows:4 .)

Алгоритм спроектирован так, чтобы всегда прерываться в наилучшей возможной точке останова (как определено в спецификации ) путем удаления правил в правильном порядке, если не все из них могут быть удовлетворены. Обратите внимание, что нам нужно переразметку не более одного раза для каждого потока фрагментации . Ко времени второго прохода макета лучшее место разрыва уже было передано алгоритмам макета. Это место разрыва, которое было обнаружено на первом проходе макета и предоставлено как часть выходных данных макета в этом раунде. На втором этапе компоновки мы не будем размещать макет до тех пор, пока у нас не закончится место — на самом деле, от нас не ожидается, что у нас закончится место (на самом деле это было бы ошибкой), потому что нам предоставили супер-приятный (ну, насколько это было приятно) место для раннего перерыва, чтобы избежать ненужного нарушения каких-либо правил нарушения. Итак, мы просто выкладываемся до этой точки и ломаемся.

В этой связи иногда нам действительно нужно нарушать некоторые запросы на предотвращение прерываний, если это помогает избежать переполнения фрагментайнера. Пример:

Здесь нам не хватает места прямо перед #second , но у него есть «break-before:avoid». Это переводится как «нарушение предотвращения разрыва», как и в последнем примере. У нас также есть NGEarlyBreak с «нарушением сирот и вдов» (внутри #first > перед «строкой 2»), что все еще не идеально, но лучше, чем «нарушение нарушения разрыва». Так что сломаемся перед «строкой 2», нарушив запрос сирот/вдов. Спецификация касается этого в 4.4. Unforced Breaks , где он определяет, какие правила нарушения игнорируются в первую очередь, если у нас недостаточно точек останова, чтобы избежать переполнения фрагментатора.

Краткое содержание

Основная функциональная цель проекта фрагментации блоков LayoutNG заключалась в том, чтобы обеспечить поддержку архитектуры LayoutNG всего, что поддерживает устаревший движок, и как можно меньше, кроме исправления ошибок. Основным исключением здесь является улучшенная поддержка предотвращения разрывов (например break-before:avoid ), поскольку это основная часть механизма фрагментации, поэтому она должна была быть там с самого начала, так как добавление ее позже означало бы еще одну перезапись. .

Теперь, когда фрагментация блоков LayoutNG завершена, мы можем начать работу над добавлением новых функций, таких как поддержка смешанных размеров страниц при печати, полей полей @page при печати, box-decoration-break:clone и т. д. И, как и в случае с LayoutNG в целом , мы ожидаем, что количество ошибок и нагрузка на обслуживание новой системы со временем будут значительно ниже.

Спасибо за прочтение!

Благодарности