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 dibuat 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 yang tersisa ditempatkan di fragmen berikutnya.

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

Fragmentasi blok analog dengan jenis fragmentasi lain yang terkenal: fragmentasi baris, atau dikenal sebagai "pemisahan baris". Setiap elemen inline yang terdiri dari lebih dari satu kata (node teks apa pun, elemen <a> apa pun, dan sebagainya), dan memungkinkan pemisahan baris, dapat dibagi menjadi beberapa fragmen. Setiap fragmen ditempatkan ke dalam kotak baris 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 dikirimkan 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, kini kami mendukung nilai'avoid' untuk properti CSS 'break-before' dan 'break-after', yang memungkinkan penulis menghindari jeda tepat setelah header. Hal ini sering kali terlihat canggung jika hal terakhir di halaman adalah header, sementara konten bagian dimulai di halaman berikutnya. Sebaiknya jeda sebelum header.

Contoh perataan judul.
Gambar 1. Contoh pertama menampilkan judul di bagian bawah halaman, contoh kedua menampilkannya 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 blok di LayoutNG kini telah selesai

Fragmentasi inti (penampung blok, termasuk tata letak baris, float, dan pemosisian di luar alur) dikirimkan di Chrome 102. Fragmentasi fleksibel dan petak dikirimkan di Chrome 103, dan fragmentasi tabel dikirimkan di Chrome 106. Terakhir, pencetakan dikirimkan 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 melakukan tata letak.

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

Menata letak semuanya dengan NG akan memungkinkan penerapan dan pengiriman fitur baru yang hanya memiliki implementasi LayoutNG (dan tidak ada mesin lama), seperti kueri penampung CSS, pemosisian anchor, MathML, dan tata letak kustom (Houdini). Untuk kueri penampung, kami mengirimkannya sedikit lebih awal, dengan peringatan kepada developer bahwa pencetakan belum didukung.

Kami merilis bagian pertama LayoutNG pada tahun 2019, yang terdiri dari tata letak penampung blok reguler, tata letak inline, float, dan pemosisian di luar alur, tetapi tidak ada dukungan untuk flex, petak, atau tabel, dan tidak ada dukungan fragmentasi blok sama sekali. Kita akan kembali menggunakan mesin tata letak lama untuk flex, petak, tabel, serta apa pun yang melibatkan fragmentasi blok. Hal ini berlaku bahkan untuk elemen blok, inline, mengambang, dan di luar alur dalam konten yang terfragmentasi—seperti yang dapat Anda lihat, mengupgrade mesin tata letak yang kompleks di tempat adalah hal yang sangat rumit.

Selain itu, pada pertengahan 2019, sebagian besar fungsi inti tata letak fragmentasi blok LayoutNG telah diimplementasikan (di balik tanda). Jadi, mengapa perlu waktu lama untuk mengirimnya? Jawaban singkatnya adalah: fragmentasi harus berjalan 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 API JavaScript yang membaca informasi tata letak, sehingga kita perlu menulis ulang data ke mesin lama dengan cara yang dipahaminya. Hal ini termasuk memperbarui struktur data multi-kolom lama, seperti LayoutMultiColumnFlowThread, dengan benar.

Deteksi dan penanganan penggantian mesin lama

Kami harus kembali ke mesin tata letak lama jika ada konten di dalamnya yang belum dapat ditangani oleh fragmentasi blok LayoutNG. Pada saat mengirimkan fragmentasi blok LayoutNG inti, yang mencakup fleksibel, 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 penampung multi-kolom, dan sebelum mengetahui node DOM mana yang akan menjadi konteks pemformatan atau tidak. Ini adalah 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.

Proses berjalan pada hierarki pohon sebelum proses paint

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 mengatasinya? Kita akan menelusuri objek tata letak dan hierarki fragmen NG secara bersamaan. Hal ini cukup rumit, karena pemetaan antara kedua hierarki bukanlah hal yang mudah.

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 diposisikan secara absolut adalah turunan langsung dari fragmen blok penampung, meskipun ada node lain dalam rantai leluhur antara turunan yang diposisikan di luar alur dan blok penampung.

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

Masalah pada mesin fragmentasi lama

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

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

Mesin lama melacak batas kolom atau halaman imajiner di strip. Cara ini memungkinkan penyortiran konten yang tidak sesuai melewati batas ke halaman atau kolom berikutnya. Misalnya, jika hanya separuh bagian atas baris yang sesuai dengan yang dianggap mesin sebagai halaman saat ini, mesin akan menyisipkan "strut penomoran halaman" untuk mendorongnya ke bawah ke posisi yang dianggap mesin sebagai bagian atas halaman berikutnya. Kemudian, sebagian besar pekerjaan fragmentasi yang sebenarnya ("pemotongan dengan gunting dan penempatan") terjadi setelah tata letak selama pra-proses gambar dan proses gambar, dengan memotong strip konten yang tinggi menjadi halaman atau kolom (dengan memotong dan menerjemahkan bagian). Hal ini membuat beberapa hal pada dasarnya tidak mungkin, seperti menerapkan transformasi dan pemosisian relatif setelah fragmentasi (yang diperlukan spesifikasi). Selain itu, meskipun ada beberapa dukungan untuk fragmentasi tabel di mesin lama, tidak ada dukungan fragmentasi fleksibel atau petak sama sekali.

Berikut adalah ilustrasi cara tata letak tiga kolom direpresentasikan secara internal di mesin lama, sebelum menggunakan gunting, penempatan, dan lem (kita memiliki tinggi yang ditentukan, sehingga hanya empat baris yang sesuai, tetapi ada beberapa ruang yang 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 menangani hal ini dengan baik:

Bayangan teks yang terpotong ditempatkan ke dalam kolom kedua.

Apakah Anda melihat bagaimana text-shadow dari baris di kolom pertama terpotong, dan ditempatkan di bagian atas kolom kedua? Hal ini 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 bahwa di mesin lama, ada pemotongan dan kebocoran kolom yang salah. Hal ini karena transformasi menurut spesifikasi seharusnya diterapkan sebagai efek pasca-tata letak, 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 tidak dipecah dengan benar 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 pengguna tidak akan merasa nyaman untuk men-scroll di wilayah non-persegi panjang. Kotak garis dan gambar adalah contoh lain dari konten monolitik. Berikut contohnya:

Jika potongan konten monolitik terlalu tinggi untuk muat di dalam kolom, mesin lama akan memotongnya secara brutal (menyebabkan perilaku yang sangat "menarik" saat mencoba men-scroll penampung 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. Fungsi ini mendukung break-inside:avoid dan orphans and widows, tetapi tidak ada dukungan untuk menghindari jeda di antara blok, misalnya jika diminta melalui break-before:avoid. Perhatikan contoh ini:

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, saudaranya #secondchild memiliki break-before:avoid, yang berarti konten tidak ingin ada jeda 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 hierarki kotak CSS secara mendalam. 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, ini adalah penyederhanaan yang berlebihan: misalnya, elemen yang diposisikan di luar alur harus muncul dari tempatnya berada di hierarki DOM ke blok penampung sebelum dapat ditata. Saya mengabaikan detail lanjutan ini di sini demi kemudahan.

Bersama dengan kotak CSS itu sendiri, LayoutNG menyediakan ruang batasan ke algoritma tata letak. Hal ini memberi algoritma informasi seperti ruang yang tersedia untuk tata letak, apakah konteks pemformatan baru dibuat, dan hasil penyingkatan 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 berhenti.

Jika fragmentasi blok terlibat, tata letak turunan harus berhenti saat jeda. Alasan pemisahan baris mencakup kehabisan ruang di halaman atau kolom, atau pemisahan baris paksa. Kemudian, kita membuat fragmen untuk node yang kita kunjungi, dan menampilkannya hingga ke root konteks fragmentasi (penampung multikolom, atau, jika dicetak, root dokumen). Kemudian, pada root konteks fragmentasi, kita bersiap untuk fragmentainer baru, dan turun ke hierarki lagi, melanjutkan dari 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 direpresentasikan. NGBlockBreakToken dilampirkan ke NGPhysicalBoxFragment yang dihasilkan untuk node yang terputus di dalamnya. Token jeda disebarkan ke induk, membentuk hierarki token jeda. Jika kita perlu melakukan pemisahan sebelum node (bukan di dalamnya), tidak akan ada fragmen yang dihasilkan, tetapi node induk masih perlu membuat token jeda "break-before" untuk node, sehingga kita dapat mulai menatanya saat mencapai posisi yang sama di hierarki node di fragmentainer berikutnya.

Jeda disisipkan saat ruang fragmentainer habis (jeda tidak dipaksa), atau saat jeda dipaksa 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 dipaksakan dengan benar, kita perlu melacak titik henti sementara yang mungkin bagus. Data ini berarti kita dapat kembali dan menggunakan titik henti sementara terbaik terakhir yang ditemukan, jika kita kehabisan ruang pada titik saat kita melanggar permintaan penghindaran jeda (misalnya, break-before:avoid atau orphans:7). Setiap titik henti sementara yang mungkin diberikan skor, mulai dari "hanya lakukan ini sebagai upaya terakhir" hingga "tempat yang sempurna untuk jeda", dengan beberapa nilai di antaranya. Jika lokasi jeda mendapatkan skor "sempurna", artinya tidak ada aturan jeda yang akan dilanggar jika kita berhenti di sana (dan jika kita mendapatkan skor ini tepat pada saat kita kehabisan ruang, tidak perlu mencari lagi yang lebih baik). Jika skornya adalah "last-resort", titik henti sementara bahkan tidak valid, tetapi kita mungkin masih berhenti di sana jika tidak menemukan yang lebih baik, untuk menghindari overflow fragmentainer.

Titik henti sementara yang valid umumnya hanya terjadi di antara saudara (kotak baris atau blok), dan bukan, misalnya, antara induk dan turunan pertamanya (titik henti sementara class C adalah pengecualian, tetapi kita tidak perlu membahasnya di sini). Misalnya, ada titik henti sementara yang valid sebelum saudara blok dengan break-before:avoid, tetapi titik henti sementara tersebut berada di antara "sempurna" dan "upaya terakhir".

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 penampung blok, maupun baris fleksibel). Kita dapat membentuk rantai atau jalur objek NGEarlyBreak, jika titik henti sementara terbaik berada di suatu tempat di dalam sesuatu yang kita lewati sebelumnya saat kehabisan ruang. Berikut contohnya:

Dalam hal ini, kita kehabisan ruang tepat sebelum #second, tetapi memiliki "break-before:avoid", yang mendapatkan skor lokasi jeda "melanggar break avoid". Pada titik tersebut, kita memiliki rantai NGEarlyBreak "inside #outer > inside #middle > inside #inner > before "line 3"', dengan "perfect", jadi sebaiknya kita berhenti di sana. Jadi, kita perlu menampilkan dan menjalankan ulang tata letak dari awal #outer (dan kali ini meneruskan 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.)

Algoritme dirancang untuk selalu berhenti pada titik henti sementara terbaik—seperti yang ditentukan dalam spesifikasi—dengan menghapus aturan dalam urutan yang benar, jika tidak semuanya dapat dipenuhi. Perhatikan bahwa kita hanya perlu menata ulang maksimal sekali per alur fragmentasi. 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. Pada tahap tata letak kedua, kita tidak akan melakukan tata letak sampai kehabisan ruang—sebenarnya kita tidak akan kehabisan ruang (itu sebenarnya akan menjadi error), karena kita telah diberi tempat yang sangat bagus (ya, sebaik yang tersedia) untuk menyisipkan jeda awal, agar tidak melanggar aturan jeda yang tidak perlu. Jadi, kita hanya menata letak hingga titik tersebut, lalu berhenti.

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 tepat sebelum #second, tetapi memiliki "break-before:avoid". Itu diterjemahkan menjadi "melanggar waktu istirahat", seperti contoh terakhir. Kita juga memiliki NGEarlyBreak dengan "melanggar orphan dan widow" (di dalam #first > sebelum "baris 2"), yang masih belum sempurna, tetapi lebih baik daripada "melanggar break avoid". Jadi, kita akan berhenti sebelum "baris 2", yang melanggar permintaan anak yatim / janda. Spesifikasi menangani 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 penghindaran jeda yang lebih baik (misalnya, break-before:avoid), karena ini adalah bagian inti dari mesin fragmentasi, sehingga 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. Dan seperti LayoutNG secara umum, kami memperkirakan rasio bug dan beban pemeliharaan sistem baru akan jauh lebih rendah dari waktu ke waktu.

Ucapan terima kasih