Szczegółowa analiza renderowania: fragmentacja bloków LayoutNG

Morten Stenshorne
Morten Stenshorne

Blokowanie fragmentacji polega na dzieleniu pola CSS na poziomie bloku (np. sekcji lub akapitu) na wiele fragmentów, które nie mieszczą się w całości wewnątrz jednego kontenera fragmentów, zwanego fragmentainerem. Fragmentacja nie jest elementem, ale reprezentuje kolumnę w układzie wielokolumnowym lub stronę w multimediach opartych na stronach.

Aby doszło do fragmentacji, treść musi znajdować się w kontekście fragmentacji. Kontekst fragmentacji jest określany najczęściej przez kontener wielokolumnowy (treść jest dzielona na kolumny) lub podczas drukowania (treść jest dzielona na strony). Długi akapit z wieloma wierszami może zostać podzielony na kilka fragmentów, tak aby pierwsze wiersze znalazły się w pierwszym fragmencie, a pozostałe – w kolejnych.

Akapit tekstu podzielony na dwie kolumny.
W tym przykładzie akapit został podzielony na 2 kolumny przy użyciu układu wielokolumnowego. Każda kolumna jest fragmentarką, która reprezentuje fragment pofragmentowanego przepływu.

Fragmentacja blokowa jest podobna do innego dobrze znanego typu fragmentacji: fragmentacji wierszy, znanego też jako „łamanie wiersza”. Każdy element wbudowany, który zawiera więcej niż 1 słowo (dowolny węzeł tekstowy, dowolny element <a> itd.), który zezwala na podziały wierszy, może zostać podzielony na kilka fragmentów. Każdy fragment jest umieszczany w innym polu wiersza. Pole wiersza to wbudowany fragment kodu, który jest odpowiednikiem fragmentatora w kolumnach i stronach.

Fragmentacja bloku LayoutNG

LayoutNGBlockFragmentation to przeredagowanie silnika fragmentacji dla LayoutNG, który jest początkowo dostępny w Chrome 102. W przypadku struktur danych zastąpiliśmy kilka struktur danych sprzed wprowadzenia nowego kraju fragmentami NG reprezentowanymi bezpośrednio w drzewie fragmentów.

Na przykład obsługujemy teraz wartość „unikaj” dla właściwości CSS „break-before” i „break-after”, dzięki czemu autorzy mogą unikać przerw tuż po nagłówku. Często wydaje się dziwne, gdy ostatnim elementem jest nagłówek, a zawartość sekcji zaczyna się na następnej stronie. Lepiej jest przerwać przed nagłówkiem.

Przykład wyrównania nagłówka.
Rysunek 1. Pierwszy przykład pokazuje nagłówek u dołu strony, a drugi u góry następnej strony.

Chrome obsługuje również nadmiar fragmentacji, dzięki czemu treści monolityczne (przypuszczalnie nierozerwalne) nie są podzielone na wiele kolumn, a efekty malowania, takie jak cienie i przekształcenia, są stosowane prawidłowo.

Ukończono fragmentację blokad w LayoutNG

Fragmentacja rdzeni (kontenery blokowe, w tym układ linii, elementy zmiennoprzecinkowe i pozycjonowanie poza przepływem) dostępna w Chrome 102. Fragmentacja Flex i siatki dostępna w Chrome 103, a fragmentacja tabel w Chrome 106. Ponadto drukowanie jest dostępne w Chrome 108. Fragmentacja bloków była ostatnią funkcją wykorzystywaną przy wykonywaniu układu, która bazowała na starszej wersji wyszukiwarki.

Od wersji Chrome 108 starsza wersja wyszukiwarki nie jest już używana do tworzenia układu.

Dodatkowo struktury danych LayoutNG obsługują malowanie i testowanie działań, ale w interfejsach API JavaScript używamy niektórych starszych struktur danych, które odczytują informacje o układzie, np. offsetLeft i offsetTop.

Wdrożenie wszystkich z wartością NG umożliwi wdrażanie i wysyłanie nowych funkcji, które korzystają tylko z implementacji LayoutNG (a nie starszych wersji silnika), takich jak zapytania dotyczące kontenerów CSS, pozycjonowanie zakotwiczonych, MathML i układ niestandardowy (Houdini). W przypadku zapytań dotyczących kontenerów wysyłaliśmy je z odpowiednim wyprzedzeniem, ale dla deweloperów ostrzegaliśmy, że drukowanie nie jest jeszcze obsługiwane.

W 2019 roku przesłaliśmy pierwszą część LayoutNG, która obejmuje standardowy układ kontenerów blokowych, układ wbudowany, elementy pływające i pozycjonowanie poza przepływem, ale nie obsługuje elastyczności, siatki i tabel ani nie obsługuje fragmentacji bloków. W przypadku elastycznych elementów, siatki, tabel i innych elementów, które obejmują fragmentację bloków, wrócilibyśmy do starszego mechanizmu układu. Dotyczyło to nawet elementów blokowych, w tekście, pływających i wychodzących poza treści w treściach fragmentarycznych. Jak widać, uaktualnienie tak złożonego silnika układu wymaga szczególnej refleksji.

Ponadto w połowie 2019 roku większość podstawowych funkcji układu fragmentacji bloków LayoutNG została już wdrożona (pod flagą). Dlaczego wysyłka trwała tak długo? Krótka odpowiedź brzmi: fragmentacja musi współistnieć z różnymi starszymi częściami systemu, których nie można usunąć ani uaktualnić, dopóki nie uaktualnią Państwo wszystkich zależności.

Interakcja ze starszą wersją wyszukiwarki

Starsze struktury danych nadal odpowiadają za interfejsy API JavaScript, które odczytują informacje o układzie, dlatego musimy odpisywać dane do starszej wyszukiwarki w sposób, który rozumie. Obejmuje to prawidłowe zaktualizowanie starszych struktur danych wielokolumnowych, takich jak LayoutMultiColumnFlowThread.

Wykrywanie i obsługa kreacji zastępczych starszej wersji wyszukiwarki

Gdy w środku znajdowały się treści, których nie można było jeszcze obsłużyć przez fragmentację bloków LayoutNG, musieliśmy wracać do starszego mechanizmu układu. W momencie dostarczania podstawowych bloków LayoutNG fragmentacji bloków, która obejmowała elementy elastyczne, siatkę, tabele i wszystko, co jest drukowane. Było to szczególnie trudne, ponieważ przed utworzeniem obiektów w drzewie układu musieliśmy wykryć konieczność korzystania ze starszych kreacji zastępczych. Musieliśmy na przykład wykryć, czy istnieje wielokolumnowy element nadrzędny kontenera, a jeszcze zanim dowiedzieliśmy się, które węzły DOM staną się kontekstem formatowania. To problem typu kurczak i jajko, który nie ma idealnego rozwiązania, ale o ile jego jedynym nieprawidłowym działaniem są fałszywe trafienia (powrót do starszej wersji, gdy w rzeczywistości nie ma takiej potrzeby), nie ma problemu, ponieważ wszelkie błędy w tym układzie są obecne w Chromium, a nie nowe.

Spacer wśród drzew

Wstępne renderowanie to coś, co robimy po układzie, ale przed malowaniem. Głównym wyzwaniem jest to, że nadal musimy przejść drzewo obiektów układu, ale mamy fragmenty NG. Jak sobie z tym radzić? Przechodzimy jednocześnie z obiektem układu i drzewami fragmentów NG. To dość skomplikowane, ponieważ mapowanie między dwoma drzewami nie jest proste.

Struktura drzewa obiektów układu jest bardzo podobna do drzewa DOM, jednak drzewo fragmentów to wyjście układu, a nie dane wejściowe. Oprócz faktycznego odzwierciedlania efektu fragmentacji, w tym fragmentacji w tekście (fragmenty wierszy) i fragmentacji bloków (fragmenty kolumn lub strony), drzewo fragmentów ma bezpośrednią relację nadrzędny-podrzędny między blokiem zawierającym element a elementami podrzędnymi DOM, których fragment jest blokiem nadrzędnym. Na przykład w drzewie fragmentów fragment wygenerowany przez absolutnie umieszczony element jest bezpośrednim elementem podrzędnym wobec niego zawierającego fragment blokowy, nawet jeśli w łańcuchu elementów nadrzędnych między elementem podrzędnym umieszczonym poza przepływem a jego elementem nadrzędnym są inne węzły.

Sprawa jest jeszcze bardziej skomplikowana, gdy wewnątrz fragmentacji znajduje się element, który znajduje się poza przepływem, ponieważ w tym przypadku fragmenty poza przepływem stają się bezpośrednimi elementami podrzędnymi fragmentainera (a nie elementem podrzędnym tego, co CSS uważa za blok nadrzędny). Aby współistnieć ze starszym mechanizmem, trzeba było go rozwiązać. W przyszłości powinniśmy być w stanie uprościć ten kod, ponieważ LayoutNG został zaprojektowany z myślą o elastycznej obsłudze wszystkich nowoczesnych trybów układu.

Problemy ze starszym mechanizmem fragmentacji

W starszym mechanizmie, zaprojektowanym we wcześniejszej erze sieci, nie ma w rzeczywistości koncepcji fragmentacji, nawet jeśli istniała wtedy również fragmentacja (z myślą o drukowaniu). Obsługa fragmentacji była po prostu czymś, co zostało wcześniej wzmocnione (drukowanie) lub zmodernizowane (wielokolumnowe).

Podczas układania treści fragmentarycznej starszy mechanizm umieszcza wszystko na długim pasku, którego szerokość odpowiada rozmiarowi kolumny lub strony. Jej wysokość odpowiada wysokości, na jaką mieści się treść. Ten wysoki pasek nie jest renderowany na stronie – traktuj go jak renderowanie wirtualnej strony, której układ zmienia się tak, by końcowo ją wyświetlić. Jest to koncepcyjnie podobne do wydrukowania całego artykułu w gazecie w jednej kolumnie, a następnie cięcia go nożyczkami na kilka części w drugim etapie. (W tamtych czasach niektóre gazety używały technik podobnych do tej!)

Starsza wyszukiwarka śledzi na pasku wymyślone granice strony lub kolumny. Dzięki temu treści, które nie mieszczą się w granicach, są przesuwane do następnej strony lub kolumny. Jeśli na przykład do tego, co według wyszukiwarki mieści się w bieżącej stronie, mieści się tylko górna połowa wiersza, zostanie wstawiona „kreska podziału na strony”, by przesunąć ją w dół do pozycji, z której wyszukiwarka zakłada, że górna część następnej strony to górna część. Następnie większość rzeczywistego fragmentacji (czyli „obcinanie nożyczkami i umiejscowienie”) następuje po wyrenderowaniu i dopasowaniu zawartości do strony (wycięcie układu i położenie strony) przez wycięcie lub wycięcie treści na stronie. Zastosowanie przekształceń i pozycjonowanie względne po fragmentacji (a tego wymaga specyfikacja) staje się w zasadzie niemożliwe. Co więcej, chociaż w starszej wersji wyszukiwarki obsługuje się fragmentację tabel, nie obsługuje ona w ogóle elastycznych ani fragmentacji siatki.

Oto ilustracja przedstawiająca wewnętrznie układ trzykolumnowy w starszej wersji wyszukiwarki, przed użyciem nożyczek, miejsca docelowego i kleju (mamy określoną wysokość, więc pasują tylko 4 wiersze, ale na dole jest trochę wolnego miejsca):

Wewnętrzna reprezentacja jako jedna kolumna z sekcjami podziału na strony w miejscach, w których treść jest ułożona, oraz 3 kolumny wyświetlane na ekranie

Ze względu na to, że starszy mechanizm układu nie dzieli treści podczas układu, występuje wiele dziwnych artefaktów, takich jak nieprawidłowe pozycjonowanie względne i przekształcenia, a także obcinanie cieni ramek przy krawędziach kolumn.

Oto przykład z parametrem text-shadow:

Starsza wersja wyszukiwarki nie radzi sobie dobrze:

Przycięte cienie tekstu umieszczone w drugiej kolumnie.

Czy widzisz, jak cień tekstu z wiersza w pierwszej kolumnie jest przycięty, a zamiast tego umieszczony na górze drugiej kolumny? Dzieje się tak, ponieważ starszy mechanizm układu nie rozpoznaje fragmentacji.

Powinien wyglądać tak:

Dwie kolumny tekstu z poprawnie wyświetlanymi cieniami.

Teraz trochę bardziej skomplikowany jest proces – przekształcenia i cienie ramki. Zwróć uwagę, że w starszej wersji wyszukiwarki występują nieprawidłowe przycięcie i spadek kolumny. Dzieje się tak, ponieważ przekształcenia z założenia powinny być stosowane jako efekt post-układ i post-fragmentacji. Przy fragmentacji LayoutNG oba te typy działają prawidłowo. Zwiększa to współpracę z przeglądarką Firefox, która od jakiegoś czasu dobrze obsługuje fragmentację, a większość testów w tym obszarze również przechodzi w tym obszarze.

Ramki są nieprawidłowo podzielone na 2 kolumny.

W starszej wersji wyszukiwarki występują też problemy z wysokimi treściami monolitycznymi. Treści są monolityczne, jeśli nie nadają się do podziału na kilka fragmentów. Elementy z przewijaniem nadmiarowym są monolityczne, ponieważ przewijanie w obszarach, które nie są prostokątne, nie ma sensu. Innymi przykładami treści monolitycznych są pola linii i obrazy. Oto przykład:

Jeśli treść monolitycznej jest zbyt wysoki, aby zmieścić się w kolumnie, starszy mechanizm będzie brutalnie pociąć treści (co przy próbie przewijania kontenera z możliwością przewijania będzie bardzo „ciekawe”):

Zamiast pozwolić na zapełnienie pierwszej kolumny (jak ma to miejsce w przypadku fragmentacji bloków LayoutNG):

ALT_TEXT_HERE

Starsza wersja wyszukiwarki obsługuje wymuszone przerwy. Na przykład <div style="break-before:page;"> wstawi podział strony przed elementem DIV, jednak wyszukiwanie optymalnych niewymuszonych podziałów jest ograniczone. Obsługuje on break-inside:avoid oraz sieroty i wdowy, ale nie ma możliwości unikania przerw między blokami, jeśli zostanie na przykład żądanie w break-before:avoid. Przeanalizuj ten przykład:

Tekst podzielony na dwie kolumny.

W tym przypadku element #multicol ma w każdej kolumnie miejsce na 5 wierszy (ponieważ ma 100 pikseli wysokości, a wysokość wiersza to 20 pikseli), więc w pierwszej kolumnie zmieści się cały element #firstchild. Jednak element równorzędny #secondchild ma parametr break-before:avoid, co oznacza, że treści nie chcą, aby przerwa między nimi nie miała miejsca. Wartość widows wynosi 2, więc musimy przenieść 2 wiersze wartości #firstchild do drugiej kolumny, aby uwzględnić wszystkie żądania uniknięcia przerwy. Chromium to pierwszy mechanizm przeglądarki, który w pełni obsługuje tę kombinację funkcji.

Jak działa fragmentacja NG

Mechanizm układu NG zwykle układa dokument, poruszając się najpierw przez drzewo ramki CSS. Po ustaleniu wszystkich elementów potomnych węzła można ukończyć jego układ, generując fragment NGPhysicalFragment i powracając do algorytmu układu nadrzędnego. Ten algorytm dodaje fragment do listy fragmentów podrzędnych, a po zakończeniu przetwarzania wszystkich fragmentów podrzędnych generuje dla siebie fragment ze wszystkimi jego fragmentami podrzędnymi. Ta metoda powoduje utworzenie drzewa fragmentów dla całego dokumentu. Jest to jednak zbyt duże uproszczenie: na przykład elementy umieszczone poza przepływem muszą znaleźć się w drzewie DOM, zanim zostaną rozmieszczone. Dla uproszczenia pomijam te zaawansowane szczegóły.

Wraz z samym polem CSS funkcja LayoutNG zapewnia przestrzeń na ograniczenia dla algorytmu układu. Dzięki temu algorytm otrzymuje takie informacje jak ilość dostępnego miejsca na układ, informacje o tym, czy został ustanowiony nowy kontekst formatowania, czy pośrednie zwijanie wyników z poprzedniej treści. Przestrzeń ograniczeń zna również rozmiar rozłożonego bloku fragmentu i bieżące odsunięcie bloków do niego. Ta opcja wskazuje, gdzie zrobić przerwę.

Po fragmentacji bloków układ elementów podrzędnych musi kończyć się na przerwie. Może to być na przykład brak miejsca na stronie lub w kolumnie albo wymuszona przerwa. Następnie tworzymy fragmenty dla odwiedzonych węzłów i wracamy aż do poziomu głównego kontekstu fragmentacji (kontenera multicola lub, w przypadku drukowania, do głównego poziomu dokumentu). Następnie na poziomie rdzenia kontekstu fragmentacji przygotowujemy się na nowy fragmentainer, a następnie ponownie opuszczamy drzewo, wznawiając pracę od miejsca, w którym została przerwana przed przerwą.

Kluczowa struktura danych zapewniająca środki do wznowienia układu po przerwie nosi nazwę NGBlockBreakToken. Zawiera wszystkie informacje potrzebne do poprawnego wznowienia układu w następnym fragmentatorze. NGBlockBreakToken jest powiązany z węzłem i tworzy drzewo NGBlockBreakToken, dzięki czemu reprezentowany jest każdy węzeł, który należy wznowić. NGBlockBreakToken jest dołączony do NGPhysicalBoxFragment wygenerowanego dla węzłów, które występują w środku, gdy występują błędy. Tokeny podziału są przekazywane do urządzeń nadrzędnych w postaci drzewa tokenów przerwania. Jeśli trzeba będzie przerwać przed węzeł (a nie wewnątrz niego), nie zostanie wygenerowany żaden fragment, ale węzeł nadrzędny wciąż musi utworzyć dla niego token podziału „przed

Przerwy są wstawiane, gdy skończy się miejsce fragmentainera (niewymuszona przerwa) lub gdy zażąda wymuszonej przerwy.

W specyfikacji znajdują się reguły dotyczące optymalnych niewymuszonych przerw, a wstawianie przerwy dokładnie tam, gdzie zabraknie miejsca, nie zawsze jest właściwym rozwiązaniem. Na przykład istnieje różne właściwości CSS, takie jak break-before, które mają wpływ na wybór miejsca przerwy.

Aby podczas tworzenia układu prawidłowo wdrożyć sekcję specyfikacji niewymuszonych przerw, musimy wykrywać potencjalnie dobre punkty przerwania. Ten rekord oznacza, że możemy się cofnąć i użyć ostatniego znalezionego punktu przerwania, jeśli zabraknie nam miejsca w miejscu, w którym naruszalibyśmy żądania unikania przerwy (np. break-before:avoid lub orphans:7). Każdy możliwy punkt przerwania otrzymuje ocenę w postaci od „zrób to w ostateczności” do „idealnego miejsca na przerwę”, z pewnymi wartościami pomiędzy. Jeśli lokalizacja przerwy na reklamę zostanie oceniona jako „idealna”, oznacza to, że nie dojdzie do naruszenia żadnych reguł, jeśli naruszymy zasady (a jeśli uzyskamy taki wynik dokładnie wtedy, gdy skończy się miejsce, nie ma potrzeby szukać czegoś lepszego). Jeśli wynik to „last-resort”, punkt przerwania nie jest nawet prawidłowy, ale i tak możemy go przerwać, jeśli nie znajdziemy niczego lepszego, co pozwoli uniknąć przepełnienia fragmentainera.

Prawidłowe punkty przerwania występują zwykle tylko pomiędzy elementami równorzędnego (ramki wiersza lub bloki), a nie np. między elementem nadrzędnym a pierwszym elementem podrzędnym (punkty przerwania klasy C stanowią wyjątek, ale nie trzeba ich tu omawiać). Istnieje prawidłowy punkt przerwania na przykład przed blokiem równorzędnym z parametrem break-before:avoid, ale mieści się on gdzieś pomiędzy „perfect” a „last-resort”.

Podczas układu śledzimy jak dotąd najlepszy punkt przerwania znaleziony w strukturze o nazwie NGEarlyBreak. Wczesna przerwa to możliwy punkt przerwania przed węzłem blokowym, wewnątrz niego lub przed linią (linią kontenera bloku lub linię elastyczną). Możemy utworzyć łańcuch lub ścieżkę obiektów NGEarlyBreak w przypadku, gdy najlepszy punkt przerwania znajdzie się gdzieś głęboko w czymś, co przeszliśmy wcześniej, gdy skończy się miejsce. Oto przykład:

W tym przypadku kończy się miejsce tuż przed #second, ale zawiera ono ciąg „break-before:avoid”, który dla lokalizacji przerwy w działaniu otrzymuje wynik „Naruszenie przerwy w unikaniu”. W tym momencie mamy łańcuch NGEarlyBreak „wewnątrz #outer > wewnątrz #middle > wewnątrz #inner > przed „linią 3””, z wartością „perfekcyjnie”, więc wolimy go przerwać. Musimy więc wrócić do układu i uruchomić go ponownie od początku fragmentu #outer (i tym razem miej odnaleziony układ NGEarlyBreak), aby móc przerwać ruch przed „wierszem 3” w ciągu #inner. (Przełamujemy je przed „wierszem 3”, tak aby pozostałe 4 wyniki znajdowały się w następnym urządzeniu fragmentalizacyjnym, co ma zwrócić uwagę widows:4).

Algorytm zaprojektowano tak, aby zawsze działał przy najlepszym możliwym punkcie przerwania – zgodnie z definicją w specyfikacji – przez pomijanie reguł we właściwej kolejności, jeśli nie wszystkie nie zostaną spełnione. Pamiętaj, że w przypadku każdego procesu fragmentacji wystarczy zmienić układ maksymalnie raz. Gdy dojdziemy do drugiego przebiegu układu, najlepsza lokalizacja przerwy na reklamę została już przekazana algorytmom układu. Jest to lokalizacja przerwy, która została wykryta w pierwszym przebiegu układu i podana jako część danych wyjściowych układu w tej rundzie. W drugim wstępie na układ nie tworzymy układu, dopóki nie skończymy miejsca – właściwie nie oczekujemy, że zabraknie miejsca (co w rzeczywistości to byłby błąd), ponieważ mamy do dyspozycji supersłodkie (chociaż dostępne) miejsce, w którym można wstawić wczesną przerwę, która pozwoli uniknąć niepotrzebnego naruszenia zasad. A więc po prostu odhaczamy ten punkt i przerwamy się.

Dlatego czasami musimy naruszać niektóre żądania unikania przerw, jeśli pomaga to uniknąć nadmiaru fragmentatora. Na przykład:

Tutaj kończy się miejsce tuż przed #second, ale pojawia się fragment „break-before:avoid”. To oznacza „naruszenie zasad unikania przerw”, tak jak w poprzednim przykładzie. Mamy też NGEarlyBreak z hasłem „naruszenia zasad dotyczących sierot i wdów” (wewnątrz #first > przed „wierszem 2”), co nie jest doskonałe, ale lepsze niż w przypadku ustawienia „Naruszenie przerwy przed zakłóceniem”. Zrobimy więc przerwę przed „wierszem 2”, naruszając żądanie sierot / wdów. Specyfikacja mówi o tym w 4.4. Niewymuszone przerwy. Określa ona, które reguły naruszające zasady są ignorowane w pierwszej kolejności, jeśli nie mamy wystarczającej liczby punktów przerwania, aby uniknąć przepełnienia fragmentatora.

Podsumowanie

Celem projektu fragmentacji bloków LayoutNG było wdrożenie wspierającej architekturę LayoutNG implementacji wszystkich funkcji obsługiwanych przez starszą wersję silnika oraz jak najmniejszej liczby dodatkowych funkcji poza poprawkami błędów. Głównym wyjątkiem jest lepsza obsługa unikania przerw (na przykład break-before:avoid), ponieważ jest to kluczowa część silnika fragmentacji, więc musi być obecna od początku, ponieważ dodanie jej później oznaczałoby kolejne przepisywanie.

Fragmentacja bloków LayoutNG została zakończona, więc możemy zacząć dodawać nowe funkcje, takie jak obsługa mieszanych rozmiarów stron podczas drukowania, @page pola marginesów podczas drukowania, box-decoration-break:clone itp. Podobnie jak w przypadku LayoutNG, oczekujemy, że z czasem częstotliwość błędów i obciążenie związane z konserwacją nowego systemu będą znacznie mniejsze.

Podziękowania