Pembahasan mendalam RenderingNG: Fragmentasi blok LayoutNG

Morten Stenshorne
Morten Stenshorne

Fragmentasi blok memisahkan kotak tingkat blok CSS (seperti bagian atau paragraf) menjadi beberapa fragmen jika tidak muat secara keseluruhan di dalam satu penampung fragmen, yang disebut fragmentainer. Fragmentainer bukanlah elemen, tetapi mewakili kolom dalam tata letak multi-kolom, atau halaman dalam media yang di-page.

Agar fragmentasi terjadi, konten harus berada dalam konteks fragmentasi. Konteks fragmentasi paling sering ditentukan oleh penampung multi-kolom (konten dibagi menjadi beberapa kolom) atau saat mencetak (konten dibagi menjadi beberapa halaman). Paragraf panjang dengan banyak baris mungkin perlu dibagi menjadi beberapa fragmen, sehingga baris pertama ditempatkan di fragmen pertama, dan baris sisanya ditempatkan dalam fragmen berikutnya.

Paragraf teks yang dibagi menjadi dua kolom.
Pada contoh ini, sebuah paragraf telah dibagi menjadi dua kolom menggunakan tata letak multi-kolom. Setiap kolom adalah fragmentainer, yang mewakili suatu fragmen dari alur yang terfragmentasi.

Fragmentasi blok setara dengan jenis fragmentasi lain yang sudah dikenal luas: fragmentasi garis, yang lebih dikenal sebagai "pemutusan baris". Elemen inline apa pun yang terdiri dari lebih dari satu kata (node teks, elemen <a> apa pun, dan sebagainya), serta mengizinkan baris baru, dapat dibagi menjadi beberapa fragmen. Setiap fragmen ditempatkan ke dalam kotak garis yang berbeda. Kotak baris adalah fragmentasi inline yang setara dengan fragmentainer untuk kolom dan halaman.

Fragmentasi blok LayoutNG

LayoutNGBlockFragmentation adalah penulisan ulang mesin fragmentasi untuk LayoutNG, yang awalnya diluncurkan di Chrome 102. Dalam hal struktur data, Google Cloud menggantikan beberapa struktur data pra-NG dengan fragmen NG yang direpresentasikan secara langsung di hierarki fragmen.

Misalnya, sekarang kami mendukung nilai'avoid' untuk properti CSS 'break-before' dan 'break-after', yang memungkinkan penulis menghindari jeda tepat setelah header. Sering kali terlihat janggal ketika hal terakhir di laman adalah {i>header<i}, sedangkan konten bagian dimulai pada halaman berikutnya. Sebaiknya jeda sebelum header.

Contoh perataan judul.
Gambar 1. Contoh pertama menunjukkan judul di bagian bawah halaman, contoh kedua menampilkan judul di bagian atas halaman berikutnya dengan konten terkait.

Chrome juga mendukung fragmentasi overflow, sehingga konten monolitik (seharusnya tidak dapat dipecahkan) tidak diiris menjadi beberapa kolom, dan efek cat seperti bayangan dan transformasi diterapkan dengan benar.

Fragmentasi pemblokiran di LayoutNG kini telah selesai

Fragmentasi inti (penampung blok, termasuk tata letak garis, float, dan pemosisian alur yang tidak sesuai) dikirimkan dalam Chrome 102. Fragmentasi petak dan fleksibel dikirimkan di Chrome 103, dan fragmentasi tabel dikirimkan di Chrome 106. Terakhir, fitur pencetakan tersedia di Chrome 108. Fragmentasi blok adalah fitur terakhir yang bergantung pada mesin lama untuk melakukan tata letak.

Mulai Chrome 108, mesin lama tidak lagi digunakan untuk membuat tata letak.

Selain itu, struktur data LayoutNG mendukung lukisan dan hit-testing, tetapi kami mengandalkan beberapa struktur data lama untuk JavaScript API yang membaca informasi tata letak, seperti offsetLeft dan offsetTop.

Dengan menata letak semuanya dengan NG, Anda dapat menerapkan dan mengirimkan fitur baru yang hanya memiliki implementasi LayoutNG (dan tidak memiliki versi mesin yang lama), seperti kueri penampung CSS, pemosisian anchor, MathML, dan tata letak kustom (Houdini). Untuk kueri container, kami mengirimkannya sedikit lebih awal, dengan peringatan kepada developer bahwa pencetakan belum didukung.

Kami mengirimkan bagian pertama LayoutNG pada tahun 2019, yang terdiri dari tata letak container blok reguler, tata letak inline, float, dan positioning out-of-flow, tetapi tidak mendukung fleksi, grid, atau tabel, dan tidak ada dukungan fragmentasi blok sama sekali. Kita akan kembali menggunakan mesin tata letak lama untuk flex, grid, tabel, serta apa pun yang melibatkan fragmentasi blok. Hal tersebut berlaku bahkan untuk elemen blok, inline, mengambang, dan tidak mengalir dalam konten yang terfragmentasi—seperti yang dapat Anda lihat, mengupgrade mesin tata letak yang kompleks seperti itu merupakan hal yang sangat sulit.

Selain itu, pada pertengahan 2019, sebagian besar fungsi inti tata letak fragmentasi blok LayoutNG sudah diimplementasikan (di belakang flag). Jadi, mengapa butuh waktu lama untuk mengirim? Jawaban singkatnya adalah: fragmentasi harus berdampingan dengan benar dengan berbagai bagian sistem yang lama, yang tidak dapat dihapus atau diupgrade sampai semua dependensi diupgrade.

Interaksi mesin lama

Struktur data lama masih bertanggung jawab atas JavaScript API yang membaca informasi tata letak, jadi kita perlu menulis kembali data ke mesin lama dengan cara yang dapat dipahaminya. Hal ini termasuk memperbarui struktur data multi-kolom lama, seperti LayoutMultiColumnFlowThread, dengan benar.

Deteksi dan penanganan penggantian mesin lama

Kita harus kembali ke mesin tata letak lama ketika ada konten di dalamnya yang belum dapat ditangani oleh fragmentasi blok LayoutNG. Pada saat pengiriman fragmentasi blok LayoutNG inti, yang mencakup fleksi, petak, tabel, dan apa pun yang dicetak. Proses ini sangat rumit karena kami harus mendeteksi perlunya penggantian lama sebelum membuat objek dalam hierarki tata letak. Misalnya, kita perlu mendeteksi sebelum mengetahui apakah ada ancestor container multi-kolom, dan sebelum kita mengetahui node DOM mana yang akan menjadi konteks pemformatan atau tidak. Ini masalah ayam dan telur yang tidak memiliki solusi sempurna, tetapi selama satu-satunya kesalahan perilakunya adalah positif palsu (berganti ke versi lama saat sebenarnya tidak diperlukan), tidak apa-apa, karena bug apa pun dalam perilaku tata letak tersebut sudah dimiliki Chromium, bukan yang baru.

Jalan-jalan di pohon sebelum mengecat

Pre-lukis adalah hal yang kita lakukan setelah tata letak, tetapi sebelum melakukan menggambar. Tantangan utamanya adalah kita masih perlu menelusuri hierarki objek tata letak, tetapi sekarang kita memiliki fragmen NG. Jadi, bagaimana cara menanganinya? Kita akan menjalankan objek tata letak dan hierarki fragmen NG secara bersamaan. Ini cukup rumit, karena pemetaan di antara dua pohon bukanlah hal yang sepele.

Meskipun struktur hierarki objek tata letak sangat mirip dengan hierarki DOM, hierarki fragmen adalah output tata letak, bukan input untuknya. Selain benar-benar mencerminkan efek dari fragmentasi apa pun, termasuk fragmentasi inline (fragmen baris) dan fragmentasi blok (fragmen kolom atau halaman), hierarki fragmen juga memiliki hubungan induk-turunan langsung antara blok yang memuat dan turunan DOM yang memiliki fragmen tersebut sebagai blok penampungnya. Misalnya, dalam hierarki fragmen, fragmen yang dihasilkan oleh elemen yang benar-benar diposisikan adalah turunan langsung dari fragmen blok yang memuatnya, meskipun ada node lain dalam rantai ancestor antara turunan yang diposisikan dengan aliran keluar dan blok yang memuatnya.

Akan lebih rumit lagi apabila ada elemen yang diposisikan di dalam fragmentasi, karena fragmen out-of-flow menjadi turunan langsung dari fragmentainer (dan bukan turunan dari yang dianggap CSS sebagai blok yang memuatnya). Masalah ini harus diselesaikan untuk dapat digunakan berdampingan dengan mesin lama. Di masa mendatang, kita seharusnya bisa menyederhanakan kode ini, karena LayoutNG didesain untuk mendukung semua mode tata letak modern secara fleksibel.

Masalah pada mesin fragmentasi lama

Mesin lama, yang didesain pada era web sebelumnya, tidak benar-benar memiliki konsep fragmentasi, meskipun fragmentasi secara teknis juga ada saat itu (untuk mendukung pencetakan). Dukungan fragmentasi hanyalah sesuatu yang dipasang di atas (pencetakan) atau diretrofit (multi-kolom).

Saat menata konten yang dapat difragmentasi, mesin lama meletakkan semuanya menjadi strip tinggi yang lebarnya sama dengan ukuran inline kolom atau halaman, dan tingginya sama tingginya dengan yang seharusnya untuk menampung kontennya. Strip tinggi ini tidak dirender ke halaman—anggap sebagai rendering ke halaman virtual yang kemudian disusun ulang untuk tampilan akhir. Caranya mirip dengan mencetak seluruh artikel koran kertas ke dalam satu kolom, lalu menggunakan gunting untuk memotongnya menjadi beberapa bagian sebagai langkah kedua. (Dahulu, beberapa surat kabar sebenarnya menggunakan teknik yang mirip dengan ini!)

Mesin lama melacak batas halaman atau kolom imajiner dalam strip. Cara ini memungkinkan penyortiran konten yang tidak sesuai melewati batas ke halaman atau kolom berikutnya. Misalnya, jika hanya separuh bagian atas baris yang akan sesuai dengan halaman saat ini, mesin akan memasukkan "langkah penomoran halaman" untuk mendorongnya ke bawah ke posisi tempat mesin mengasumsikan bahwa bagian atas halaman berikutnya berada. Kemudian, sebagian besar pekerjaan fragmentasi sebenarnya ("memotong dengan gunting dan penempatan") terjadi setelah tata letak selama pra-lukisan dan mengecat, dengan memotong halaman yang tinggi menjadi potongan halaman dan melukisnya. Hal ini membuat beberapa hal yang pada dasarnya tidak mungkin, seperti menerapkan transformasi dan pemosisian relatif setelah fragmentasi (yang diperlukan oleh spesifikasi). Selain itu, meskipun ada beberapa dukungan untuk fragmentasi tabel di mesin lama, tidak ada dukungan fragmentasi grid atau fleksibel sama sekali.

Berikut adalah ilustrasi bagaimana tata letak tiga kolom direpresentasikan secara internal di mesin lama, sebelum menggunakan gunting, penempatan, dan lem (kami memiliki ketinggian tertentu, sehingga hanya empat garis yang muat, tetapi ada ruang berlebih di bagian bawah):

Representasi internal sebagai satu kolom dengan strut penomoran halaman tempat konten dipisahkan, dan representasi di layar sebagai tiga kolom

Karena mesin tata letak lama sebenarnya tidak fragmen konten selama tata letak, maka ada banyak artefak aneh, seperti pemosisian relatif dan transformasi yang diterapkan dengan tidak benar, dan bayangan kotak terpotong di tepi kolom.

Berikut adalah contoh dengan text-shadow:

Mesin lama tidak dapat menangani hal ini dengan baik:

Bayangan teks yang terpotong ditempatkan ke dalam kolom kedua.

Apakah Anda melihat bagaimana bayangan teks dari baris di kolom pertama diklip, dan ditempatkan di bagian atas kolom kedua? Itu karena mesin tata letak lama tidak memahami fragmentasi.

Hasilnya akan terlihat seperti berikut:

Dua kolom teks dengan bayangan ditampilkan dengan benar.

Selanjutnya, mari kita membuatnya sedikit lebih rumit, dengan transformasi dan {i>box-shadow<i}. Perhatikan bagaimana pada mesin lama, ada clipping dan column bleed yang salah. Hal itu karena transformasi didasarkan pada spesifikasi yang seharusnya diterapkan sebagai efek pasca-fragmentasi pasca-fragmentasi. Dengan fragmentasi LayoutNG, keduanya berfungsi dengan benar. Hal ini meningkatkan interop dengan Firefox, yang memiliki dukungan fragmentasi yang baik untuk beberapa waktu dengan sebagian besar pengujian di area ini juga lulus di sana.

Kotak salah rusak di dua kolom.

Mesin lama juga memiliki masalah dengan konten monolitik yang tinggi. Konten bersifat monolitik jika tidak memenuhi syarat untuk dipecah menjadi beberapa fragmen. Elemen dengan scroll tambahan bersifat monolitik, karena tidak masuk akal bagi pengguna untuk men-scroll di area non-persegi panjang. Kotak garis dan gambar adalah contoh lain dari konten monolitik. Berikut contohnya:

Jika bagian konten monolitik terlalu tinggi untuk dimuat di dalam kolom, mesin lama akan membaginya secara brutal (yang menghasilkan perilaku yang sangat "menarik" saat mencoba men-scroll container yang dapat di-scroll):

Daripada membiarkannya meluap dari kolom pertama (seperti halnya dengan fragmentasi blok LayoutNG):

ALT_TEXT_HERE

Mesin lama mendukung jeda paksa. Misalnya, <div style="break-before:page;"> akan menyisipkan batas halaman sebelum DIV. Namun, ini hanya memiliki dukungan terbatas untuk menemukan jeda unforced yang optimal. Model ini mendukung break-inside:avoid serta anak yatim dan janda, tetapi tidak ada dukungan untuk menghindari jeda antar-blok, misalnya jika diminta melalui break-before:avoid. Perhatikan contoh berikut:

Teks yang dibagi menjadi dua kolom.

Di sini, elemen #multicol memiliki ruang untuk 5 baris di setiap kolom (karena tingginya 100 piksel, dan tinggi barisnya 20 piksel), sehingga semua #firstchild dapat muat di kolom pertama. Namun, #secondchild seinduknya memiliki jeda sebelum:dihindari, yang berarti konten menginginkan jeda tidak terjadi di antara keduanya. Karena nilai widows adalah 2, kita perlu mendorong 2 baris #firstchild ke kolom kedua, untuk memenuhi semua permintaan penghindaran jeda. Chromium adalah mesin browser pertama yang sepenuhnya mendukung kombinasi fitur ini.

Cara kerja fragmentasi NG

Mesin tata letak NG umumnya menata letak dokumen dengan menelusuri kedalaman pohon kotak CSS terlebih dahulu. Jika semua turunan node sudah ditata, tata letak node tersebut dapat diselesaikan, dengan menghasilkan NGPhysicalFragment dan kembali ke algoritme tata letak induk. Algoritma tersebut menambahkan fragmen ke daftar fragmen turunannya, dan, setelah semua turunan selesai, menghasilkan fragmen untuk dirinya sendiri dengan semua fragmen turunannya di dalamnya. Dengan metode ini, pembuatan hierarki fragmen untuk seluruh dokumen. Namun, hal ini terlalu penyederhanaan: misalnya, elemen posisi yang tidak mengalir (out-flow) harus bergelembung dari tempat elemen tersebut berada di hierarki DOM ke blok penampungnya sebelum dapat ditata. Saya mengabaikan detail lanjutan ini di sini agar lebih praktis.

Bersama dengan kotak CSS itu sendiri, LayoutNG menyediakan ruang batasan untuk algoritme tata letak. Hal ini memberi algoritma informasi seperti ruang yang tersedia untuk tata letak, apakah konteks pemformatan baru telah ditetapkan, dan hasil penciutan margin perantara dari konten sebelumnya. Ruang batasan juga mengetahui ukuran blok fragmentainer yang ditata, dan offset blok saat ini ke dalamnya. Ini menunjukkan tempat untuk beristirahat.

Jika fragmentasi blok terlibat, tata letak turunan harus berhenti saat jeda. Alasan pelanggaran meliputi kehabisan ruang di halaman atau kolom, atau jeda paksa. Selanjutnya, kita menghasilkan fragmen untuk node yang telah kita kunjungi, dan kembali hingga ke root konteks fragmentasi (penampung multikol, atau, untuk pencetakan, root dokumen). Kemudian, pada root konteks fragmentasi, kita mempersiapkan fragmentainer baru, dan turun ke hierarki lagi, melanjutkan bagian yang terakhir kita tinggalkan sebelum jeda.

Struktur data penting untuk menyediakan cara melanjutkan tata letak setelah jeda disebut NGBlockBreakToken. File ini berisi semua informasi yang diperlukan untuk melanjutkan tata letak dengan benar di fragmentainer berikutnya. NGBlockBreakToken dikaitkan dengan node, dan membentuk hierarki NGBlockBreakToken, sehingga setiap node yang perlu dilanjutkan akan ditampilkan. NGBlockBreakToken disertakan ke NGPhysicalBoxFragment yang dibuat untuk node yang menerobos masuk. Token jeda disebarluaskan ke induk, membentuk pohon token jeda. Jika kita perlu memecah sebelum node (bukan di dalamnya), tidak ada fragmen yang akan dihasilkan, tetapi node induk masih perlu membuat token jeda "break-sebelum" untuk node, agar kita dapat mulai menatanya saat mencapai posisi yang sama pada pohon node di fragmentainer berikutnya.

Jeda akan disisipkan saat kita kehabisan ruang fragmentainer (jeda tanpa paksa), atau saat jeda paksa diminta.

Ada aturan dalam spesifikasi untuk jeda tidak paksa yang optimal dan hanya memasukkan jeda secara persis di tempat kita kehabisan ruang tidak selalu merupakan hal yang benar untuk dilakukan. Misalnya, ada berbagai properti CSS seperti break-before yang memengaruhi pilihan lokasi jeda.

Selama tata letak, untuk menerapkan bagian spesifikasi jeda tidak paksa dengan benar, kita harus melacak titik henti sementara yang mungkin bagus. Data ini berarti kita dapat kembali dan menggunakan titik henti sementara terbaik yang ditemukan, jika kehabisan ruang pada titik tempat kita melanggar permintaan pencegahan jeda (misalnya break-before:avoid atau orphans:7). Setiap titik henti sementara yang memungkinkan akan diberi skor, mulai dari "hanya lakukan ini sebagai upaya terakhir" hingga "tempat sempurna untuk beristirahat", dengan beberapa nilai di antaranya. Jika skor lokasi jeda ditetapkan sebagai "sempurna", artinya tidak ada aturan pelanggaran yang akan dilanggar jika kita melanggarnya (dan jika kita mendapatkan skor ini tepat saat kehabisan ruang, tidak perlu melihat kembali untuk sesuatu yang lebih baik). Jika skornya adalah "last-resort", titik henti sementara bahkan bukan yang valid, tetapi kita mungkin masih akan berhenti di sana jika tidak menemukan yang lebih baik, untuk menghindari overflow fragmentainer.

Titik henti sementara yang valid umumnya hanya terjadi di antara yang seinduk (kotak baris atau blok), dan bukan, misalnya, antara induk dan turunan pertamanya (titik henti sementara kelas C adalah pengecualian, tetapi kita tidak perlu membahasnya di sini). Ada titik henti sementara yang valid, misalnya sebelum blok yang seinduk dengan break-before:avoid, tetapi titik itu berada di antara "sempurna" dan "last-resort".

Selama tata letak, kita melacak titik henti sementara terbaik yang ditemukan sejauh ini dalam struktur yang disebut NGEarlyBreak. Jeda awal adalah kemungkinan titik henti sementara sebelum atau di dalam node blok, atau sebelum baris (baik baris container blok maupun garis fleksibel). Kita mungkin membentuk rantai atau jalur objek NGEarlyBreak, jika titik henti sementara terbaik ada di bagian dalam sesuatu yang kita lalui sebelumnya saat kita kehabisan ruang. Berikut contohnya:

Dalam kasus ini, kita kehabisan ruang tepat sebelum #second, tetapi instance ini memiliki "break-before:avoid", yang mendapatkan skor lokasi jeda "melanggar waktu jeda". Pada tahap itu, kita memiliki rantai NGEarlyBreak "di dalam #outer > di dalam #middle > di dalam #inner > sebelum "baris 3"', dengan "sempurna", jadi sebaiknya kita berhenti di sana. Jadi kita perlu mengembalikan dan menjalankan kembali tata letak dari awal #outer (dan kali ini teruskan NGEarlyBreak yang kita temukan), sehingga kita dapat berhenti sebelum "baris 3" di #inner. (Kita jeda sebelum "baris 3", sehingga 4 baris yang tersisa berakhir di fragmentainer berikutnya, dan untuk menghormati widows:4.)

Algoritma ini dirancang untuk selalu rusak pada titik henti sementara terbaik—seperti yang ditentukan dalam spesifikasi—dengan meletakkan aturan dalam urutan yang benar, jika tidak semuanya dapat dipenuhi. Perhatikan bahwa kita hanya perlu menata ulang paling banyak sekali per alur fragmen. Pada saat kita berada di tahap tata letak kedua, lokasi jeda terbaik telah diteruskan ke algoritme tata letak, ini adalah lokasi jeda yang ditemukan dalam penerusan tata letak pertama, dan disediakan sebagai bagian dari output tata letak di babak tersebut. Dalam tahap tata letak kedua, kita tidak membuat tata letak sampai kehabisan ruang—sebenarnya kita tidak diharapkan kehabisan ruang (itu sebenarnya akan menjadi error), karena kita telah diberi tempat yang sangat manis (yah, semanis mungkin) untuk menyisipkan jeda lebih awal, untuk menghindari pelanggaran aturan yang tidak perlu. Jadi kita langsung buat garis besarnya, dan berhenti di situ.

Oleh karena itu, terkadang kami perlu melanggar beberapa permintaan penghindaran jeda, jika hal tersebut dapat membantu menghindari overflow fragmentainer. Contoh:

Di sini, kita kehabisan ruang sebelum #second, tetapi terdapat "break-before:avoid". Itu diterjemahkan menjadi "melanggar waktu istirahat", seperti contoh terakhir. Kami juga memiliki NGEarlyBreak dengan "melanggar anak yatim dan janda" (di dalam #first > sebelum "baris 2"), yang masih belum sempurna, tetapi lebih baik daripada "melanggar hindari jeda". Jadi, kita akan berhenti sebelum "baris 2", melanggar permintaan anak yatim / janda. Spesifikasi akan membahas hal ini di 4.4. Jeda Tidak Paksa, yang menentukan aturan pelanggaran mana yang akan diabaikan terlebih dahulu jika kita tidak memiliki titik henti sementara yang cukup untuk menghindari overflow fragmentainer.

Kesimpulan

Tujuan fungsional project fragmentasi blok LayoutNG adalah untuk menyediakan implementasi yang mendukung arsitektur LayoutNG untuk segala hal yang didukung oleh mesin lama, dan melakukan sesedikit mungkin hal selain perbaikan bug. Pengecualian utamanya adalah dukungan pencegahan gangguan yang lebih baik (misalnya, break-before:avoid), karena ini adalah bagian inti dari mesin fragmentasi, jadi ini harus ada di sana sejak awal, karena menambahkannya nanti akan berarti penulisan ulang lainnya.

Setelah fragmentasi blok LayoutNG selesai, kita dapat mulai menambahkan fungsi baru, seperti mendukung ukuran halaman campuran saat mencetak, kotak margin @page saat mencetak, box-decoration-break:clone, dan lainnya. Seperti halnya LayoutNG pada umumnya, kami memperkirakan tingkat bug dan beban pemeliharaan sistem baru akan jauh lebih rendah dari waktu ke waktu.

Ucapan terima kasih