La frammentazione dei blocchi consiste nel suddividere una casella a livello di blocco CSS (ad esempio una sezione o un paragrafo) in più frammenti quando non si adatta come un'unità all'interno di un contenitore di frammenti, chiamato fragmentainer. Un fragmentainer non è un elemento, ma rappresenta una colonna in un layout a più colonne o una pagina in contenuti multimediali impaginati.
Affinché si verifichi la frammentazione, i contenuti devono trovarsi in un contesto di frammentazione. Un contesto di frammentazione viene stabilito più comunemente da un contenitore con più colonne (i contenuti sono suddivisi in colonne) o durante la stampa (i contenuti sono suddivisi in pagine). Un paragrafo lungo con molte righe potrebbe dover essere suddiviso in più frammenti, in modo che le prime righe vengano inserite nel primo frammento e le righe rimanenti nei frammenti successivi.
La frammentazione dei blocchi è analoga a un altro tipo noto di frammentazione: la frammentazione delle linee, altrimenti nota come "interruzione di riga". Qualsiasi elemento in linea composto da più di una parola (qualsiasi nodo di testo, qualsiasi elemento <a>
e così via) e che consenta interruzioni di riga può essere suddiviso in più frammenti. Ogni frammento viene inserito in una casella di riga diversa. Un riquadro di riga è la frammentazione in linea equivalente a un riquadro di frammentazione per colonne e pagine.
Frammentazione dei blocchi LayoutNG
LayoutNGBlockFragmentation è una riscrittura del motore di frammentazione per LayoutNG, inizialmente disponibile in Chrome 102. In termini di strutture di dati, ha sostituito diverse strutture di dati precedenti a NG con frammenti NG rappresentati direttamente nell'albero dei frammenti.
Ad esempio, ora supportiamo il valore "avoid" per le proprietà CSS "break-before" e "break-after", che consentono agli autori di evitare interruzioni subito dopo un'intestazione. Spesso sembra strano quando l'ultima cosa in una pagina è un'intestazione, mentre i contenuti della sezione iniziano nella pagina successiva. È meglio inserire un a capo prima dell'intestazione.
Chrome supporta anche il sovraccarico della frammentazione, in modo che i contenuti monolitici (che dovrebbero essere indivisibili) non vengano suddivisi in più colonne e gli effetti di pittura come ombre e trasformazioni vengano applicati correttamente.
La frammentazione dei blocchi in LayoutNG è stata completata
La frammentazione di base (contenitori di blocco, inclusi layout di riga, elementi in primo piano e posizionamento fuori dal flusso) è stata rilasciata in Chrome 102. La frammentazione di Flex e della griglia è stata rilasciata in Chrome 103 e la frammentazione della tabella in Chrome 106. Infine, la stampa è disponibile con Chrome 108. La frammentazione dei blocchi era l'ultima funzionalità che dipendeva dal motore precedente per l'esecuzione del layout.
A partire da Chrome 108, il motore precedente non viene più utilizzato per eseguire il layout.
Inoltre, le strutture di dati di LayoutNG supportano la pittura e i test di hit, ma ci basiamo su alcune strutture di dati precedenti per le API JavaScript che leggono le informazioni sul layout, come offsetLeft
e offsetTop
.
La disposizione di tutto con NG consentirà di implementare e rilasciare nuove funzionalità che hanno solo implementazioni di LayoutNG (e nessuna controparte del motore precedente), come le query dei contenitori CSS, il posizionamento dell'ancora, MathML e il layout personalizzato (Houdini). Per le query dei contenitori, l'abbiamo rilasciato un po' in anticipo, con un avviso per gli sviluppatori che la stampa non era ancora supportata.
Abbiamo rilasciato la prima parte di LayoutNG nel 2019, che consisteva in un layout contenitore di blocchi regolare, layout in linea, elementi in primo piano e posizionamento fuori flusso, ma senza supporto per flex, griglie o tabelle e senza alcun supporto per la frammentazione dei blocchi. Ricorriamo all'utilizzo del motore di layout precedente per i formati flessibili, griglia e per le tabelle, oltre a tutto ciò che comporta la frammentazione dei blocchi. Questo vale anche per gli elementi a blocchi, in linea, mobili e out-of-flow all'interno di contenuti frammentati: come puoi vedere, eseguire l'upgrade in presenza di un motore di layout così complesso è un ballo molto delicato.
Inoltre, entro la metà del 2019 la maggior parte delle funzionalità di base del layout di frammentazione dei blocchi di LayoutNG era già stata implementata (dietro un flag). Perché la spedizione ha richiesto così tanto tempo? La risposta breve è: la frammentazione deve coesistere correttamente con varie parti legacy del sistema, che non possono essere rimosse o sottoposte ad upgrade finché non viene eseguito l'upgrade di tutte le dipendenze.
Interazione con il motore legacy
Le strutture di dati legacy sono ancora responsabili delle API JavaScript che leggono le informazioni sul layout, quindi dobbiamo riscrivere i dati al motore legacy in modo che possa comprenderli. Ciò include l'aggiornamento corretto delle strutture di dati multicolonna precedenti, come LayoutMultiColumnFlowThread.
Rilevamento e gestione del fallback del motore legacy
Abbiamo dovuto fare ricorso al motore di layout precedente quando i contenuti non potevano ancora essere gestiti dalla frammentazione dei blocchi di LayoutNG. Al momento dell'invio, la frammentazione dei blocchi di LayoutNG di base includeva flex, griglie, tabelle e tutto ciò che viene stampato. Questo è stato particolarmente complicato perché dovevamo rilevare la necessità di un fallback legacy prima di creare oggetti nella struttura ad albero del layout. Ad esempio, dovevamo rilevare prima di sapere se esisteva un contenitore multicolonna e prima di sapere quali nodi DOM sarebbero diventati o meno un contesto di formattazione. Si tratta di un problema di pollo e uova che non ha una soluzione perfetta, ma finché il suo unico comportamento scorretto è costituito da falsi positivi (il fallback alla versione precedente quando in realtà non ce n'è più bisogno), va bene, perché eventuali bug nel comportamento del layout sono quelli già esistenti in Chromium, non nuovi.
Passeggiata tra gli alberi prima della verniciatura
La pre-pittura è un'operazione che eseguiamo dopo il layout, ma prima della pittura. La sfida principale è che dobbiamo ancora esaminare l'albero degli oggetti del layout, ma ora abbiamo i frammenti NG. Come possiamo risolvere il problema? Esaminiamo contemporaneamente l'oggetto layout e le strutture ad albero dei frammenti NG. Questo è abbastanza complicato, perché la mappatura tra i due alberi non è banale.
Sebbene la struttura ad albero dell'oggetto layout assomigli molto a quella dell'albero DOM, l'albero dei frammenti è un'uscita del layout, non un input. Oltre a riflettere l'effetto di qualsiasi frammentazione, inclusa la frammentazione in linea (frammenti di riga) e la frammentazione in blocchi (colonne o frammenti di pagina), l'albero dei frammenti ha anche una relazione diretta genitore-figlio tra un blocco contenente e i discendenti DOM che hanno quel frammento come blocco contenente. Ad esempio, nell'albero dei frammenti, un frammento generato da un elemento con posizionamento assoluto è un elemento secondario diretto del frammento del blocco contenente, anche se nella catena di ascendenza sono presenti altri nodi tra il discendente posizionato fuori dal flusso e il blocco contenente.
Può essere ancora più complicato in presenza di un elemento posizionato fuori flusso all'interno della frammentazione, perché i frammenti out-of-flow diventano figli diretti del fragmentainer (e non figli di ciò che CSS pensa sia il blocco contenitore). Si trattava di un problema che doveva essere risolto per coesistere con il motore legacy. In futuro, dovremmo essere in grado di semplificare questo codice, perché LayoutNG è progettato per supportare in modo flessibile tutte le modalità di layout moderne.
I problemi del motore di frammentazione legacy
Il motore legacy, progettato in un'epoca precedente del web, in realtà non ha un concetto di frammentazione, anche se tecnicamente esisteva anche quella di allora (per supportare la stampa). Il supporto della frammentazione è stato semplicemente aggiunto (stampa) o adattato (più colonne).
Quando impagina i contenuti frazionabili, il motore precedente li dispone in una striscia alta la cui larghezza corrisponde alle dimensioni in linea di una colonna o di una pagina e l'altezza è sufficiente per contenere i contenuti. Questa striscia alta non viene visualizzata nella pagina, ma in una pagina virtuale che viene poi riorganizzata per la visualizzazione finale. È concettualmente simile alla stampa di un intero articolo di giornale cartaceo in una colonna e all'utilizzo delle forbici per ritagliarlo in più pezzi come secondo passaggio. (In passato, alcuni giornali utilizzavano effettivamente tecniche simili a queste).
Il motore precedente tiene traccia di un confine immaginario di pagina o colonna nella striscia. In questo modo, i contenuti che non rientrano nel limite vengono spostati nella pagina o nella colonna successiva. Ad esempio, se solo la metà superiore di una linea può adattarsi alla pagina corrente, viene inserito un "punto di paginazione" per spingerlo verso il basso nella posizione in cui il motore presuppone che si trovi la parte superiore della pagina successiva. Quindi, la maggior parte del lavoro di frammentazione effettiva (il "taglio con forbici e il posizionamento") avviene dopo il layout durante il pre-paint e la pittura, taglio dei contenuti alti o delle strisce. Ciò ha reso alcune cose sostanzialmente impossibili, come l'applicazione di trasformazioni e posizionamento relativo dopo la frammentazione (come richiesto dalle specifiche). Inoltre, sebbene il motore precedente supporti in parte la frammentazione delle tabelle, non supporta affatto la frammentazione delle griglie o dei layout flessibili.
Ecco un'illustrazione di come un layout a tre colonne viene rappresentato internamente nell'engine precedente, prima di utilizzare forbici, posizionamento e colla (abbiamo un'altezza specificata, quindi si adattano solo quattro righe, ma c'è un po' di spazio in eccesso in basso):
Poiché il motore di layout precedente non frammenta effettivamente i contenuti durante il layout, si verificano molti strani artefatti, come l'applicazione errata di posizionamenti e trasformazioni relativi e l'accorciamento delle ombre delle caselle agli angoli delle colonne.
Ecco un esempio con text-shadow:
Il motore precedente non gestisce bene questa situazione:
Hai notato che l'ombra del testo della riga nella prima colonna viene tagliata e posizionata nella parte superiore della seconda colonna? Questo accade perché il motore di layout precedente non comprende la frammentazione.
Dovrebbe avere il seguente aspetto:
Ora rendiamo la situazione un po' più complicata con le trasformazioni e l'ombra della casella. Nota che nell'engine precedente sono presenti ritagli e fuoriuscite di colonne errati. Questo perché, secondo le specifiche, le trasformazioni dovrebbero essere applicate come effetto post-layout e post-frammentazione. Con la frammentazione LayoutNG, entrambi funzionano correttamente. Ciò aumenta l'interoperabilità con Firefox, che da un po' di tempo supporta la frammentazione con un buon supporto, con il superamento della maggior parte dei test in quest'area.
Il motore legacy presenta anche problemi con contenuti monolitici alti. I contenuti sono monolitici se non sono idonei alla suddivisione in più frammenti. Gli elementi con scorrimento con overflow sono monolitici, perché non ha senso per gli utenti scorrere in una regione non rettangolare. Le caselle di testo e le immagini sono altri esempi di contenuti monolitici. Ecco un esempio:
Se i contenuti monolitici sono troppo alti per essere inseriti in una colonna, il motore legacy la suddividerà in modo brutale (comportando un comportamento molto "interessante" quando tenti di far scorrere il contenitore scorrevole):
Invece di andare oltre la prima colonna (come accade con la frammentazione dei blocchi di LayoutNG):
Il motore precedente supporta le interruzioni forzate. Ad esempio, <div style="break-before:page;">
inserirà un'interruzione di pagina prima del DIV. Tuttavia, il supporto per trovare interruzioni non forzate ottimali è limitato. Supporta break-inside:avoid
e paragrafi orfani e vedove, ma non è possibile evitare interruzioni tra i blocchi, ad esempio se richieste tramite break-before:avoid
. Considera questo esempio:
In questo caso, l'elemento #multicol
ha spazio per 5 righe in ogni colonna (perché è alto 100 px e l'altezza della riga è 20 px), quindi tutti i valori #firstchild
potrebbero rientrare nella prima colonna. Tuttavia, il suo elemento fratello #secondchild
ha break-before:avoid, il che significa che i contenuti non vogliono che tra di loro si verifichi un'interruzione. Poiché il valore di widows
è 2, dobbiamo inviare 2 righe di #firstchild
alla seconda colonna per soddisfare tutte le richieste di evitamento delle interruzioni. Chromium è il primo motore del browser che supporta completamente questa combinazione di funzionalità.
Come funziona la frammentazione NG
In genere, il motore di layout NG esegue il layout del documento attraversando l'albero delle caselle CSS in ordine di profondità. Quando tutti i discendenti di un nodo sono disposti, il layout del nodo può essere completato producendo un NGPhysicalFragment e tornando all'algoritmo di layout principale. L'algoritmo aggiunge il frammento al proprio elenco di frammenti secondari e, una volta completati tutti i frammenti secondari, genera un frammento per sé con tutti i frammenti secondari al suo interno. Con questo metodo viene creata una struttura ad albero di frammenti per l'intero documento. Tuttavia, si tratta di una semplificazione eccessiva: ad esempio, gli elementi posizionati fuori dal flusso dovranno risalire dalla posizione in cui si trovano nella struttura a albero DOM al blocco contenente prima di poter essere disposti. Per semplicità, ignoro questo dettaglio avanzato.
Oltre alla casella CSS stessa, LayoutNG fornisce uno spazio di vincolo a un algoritmo di layout. Ciò fornisce all'algoritmo informazioni come lo spazio disponibile per il layout, se è stato stabilito un nuovo contesto di formattazione e il margine intermedio che comprime i risultati dei contenuti precedenti. Lo spazio dei vincoli conosce anche le dimensioni del blocco del frammentatore e l'offset del blocco corrente al suo interno. Indica dove interrompere.
Quando è prevista la frammentazione dei blocchi, il layout dei discendenti deve fermarsi in un punto di interruzione. I motivi della rottura includono lo spazio insufficiente nella pagina o nella colonna o una rottura forzata. Produciamo quindi frammenti per i nodi che abbiamo visitato e torniamo fino alla radice del contesto di frammentazione (il contenitore multicolonna o, in caso di stampa, la radice del documento). Poi, nel contesto principale della frammentazione, ci prepariamo per un nuovo frammentatore e scendiamo di nuovo nell'albero, riprendendo da dove avevamo interrotto prima dell'interruzione.
La struttura dei dati cruciale per fornire i mezzi per riprendere il layout dopo una pausa è chiamata NGBlockBreakToken. Contiene tutte le informazioni necessarie per riprendere correttamente il layout nel frammentatore successivo. Un NGBlockBreakToken è associato a un nodo e forma un albero di NGBlockBreakToken, in modo che ogni nodo che deve essere ripreso sia rappresentato. Un NGBlockBreakToken è collegato all'elemento NGPhysicalBoxFragment generato per i nodi che si rompono all'interno. I token di interruzione vengono propagati agli elementi padre, formando una struttura di token di interruzione. Se dobbiamo inserire un a capo prima di un nodo (anziché all'interno), non verrà prodotto alcun frammento, ma il nodo principale deve comunque creare un token di interruzione "break-before" per il nodo, in modo da poter iniziare a eseguire il layout quando raggiungiamo la stessa posizione nella struttura ad albero dei nodi nel successivo contenitore di frammenti.
Le interruzioni vengono inserite quando non è più disponibile spazio nel frammentatore (interruzione non forzata) o quando viene richiesta un'interruzione forzata.
Le specifiche prevedono regole per interruzioni ottimali non forzate e non è sempre opportuno inserire un'interruzione esattamente dove non c'è più spazio. Ad esempio, esistono varie proprietà CSS come break-before
che influiscono sulla scelta della posizione dell'interruzione.
Durante il layout, per implementare correttamente la sezione delle specifiche relativa alle interruzioni forzate, dobbiamo tenere traccia delle potenziali interruzioni. Questo record significa che possiamo tornare indietro e utilizzare l'ultimo punto di interruzione migliore trovato, se non abbiamo più spazio in un punto in cui violeremmo le richieste di evitamento delle interruzioni (ad esempio break-before:avoid
o orphans:7
). A ogni possibile punto di interruzione viene assegnato un punteggio, che va da "esegui questa operazione solo come ultima risorsa" a "luogo perfetto per l'interruzione", con alcuni valori intermedi. Se il punteggio della posizione di un'interruzione è "perfetto", significa che non verrà violata alcuna regola che viola le norme (e se otteniamo questo punteggio esattamente nel punto in cui si esaurisce lo spazio, non c'è bisogno di cercare qualcosa di migliore). Se il punteggio è "last-resort", il punto di interruzione non è nemmeno valido, ma potremmo interromperlo se non troviamo nulla di meglio, per evitare l'overflow di fragmentainer.
In genere, i punti di interruzione validi si verificano solo tra elementi fratelli (caselle di riga o blocchi) e non, ad esempio, tra un elemento principale e il suo primo elemento figlio (i punti di interruzione di classe C sono un'eccezione, ma non è necessario discuterne qui). Ad esempio, esiste un punto di interruzione valido prima di un gemello di blocco con break-before:avoid, ma si trova a metà strada tra "perfect" e "last-resort".
Durante il layout, teniamo traccia del breakpoint migliore trovato finora in una struttura chiamata NGEarlyBreak. Un'interruzione anticipata è un possibile punto di interruzione prima o all'interno di un nodo di blocco o prima di una riga (una riga del contenitore di blocchi o una riga flessibile). Potremmo formare una catena o un percorso di oggetti NGEarlyBreak, nel caso in cui il punto di interruzione migliore si trovi in un punto molto lontano di qualcosa che abbiamo esaminato in precedenza quando abbiamo esaurito lo spazio. Ecco un esempio:
In questo caso, lo spazio finisce proprio prima di #second
, ma è presente "break-before:avoid", che ottiene un punteggio di posizione dell'interruzione pari a "violating break avoid". A quel punto abbiamo una catena NGEarlyBreak di "all'interno di #outer
> all'interno di #middle
> all'interno di #inner
> prima di "riga 3"", con "perfetto", quindi preferiamo interrompere lì. Dobbiamo quindi restituire ed eseguire nuovamente il layout dall'inizio di #outer (e questa volta passare l'elemento NGEarlyBreak che abbiamo trovato), in modo da poter interrompere prima della "riga 3" in #inner. Interrompiamo prima della "riga 3", in modo che le 4 righe rimanenti finiscano nel successivo frammentatore e per rispettare widows:4
.
L'algoritmo è progettato per rompere sempre nel miglior punto di interruzione possibile, come definito nella specifica, eliminando le regole nell'ordine corretto, se non sono soddisfatte tutte. Tieni presente che dobbiamo eseguire il nuovo layout solo al massimo una volta per flusso di frammentazione. Quando siamo nella seconda elaborazione del layout, la posizione dell'interruzione migliore è già stata passata agli algoritmi di layout, ovvero la posizione dell'interruzione rilevata nella prima elaborazione del layout e fornita nell'output del layout in quel round. Nella seconda passata di layout, non eseguiamo il layout finché non esauriamo lo spazio, in realtà non è previsto che lo spazio finisca (infatti sarebbe un errore), perché ci è stato fornito un posto super-perfetto (beh, il più perfetto possibile) per inserire un'interruzione anticipata, per evitare di violare inutilmente le regole di interruzione. Quindi, li impostiamo fino a quel punto e interrompiamo.
A questo proposito, a volte dobbiamo violare alcune richieste di evasione delle interruzioni, se ciò contribuisce a evitare l'overflow di fragmentainer. Ad esempio:
Qui lo spazio finisce proprio prima di #second
, ma è presente "break-before:avoid". Questo viene tradotto in "violazione dell'evitamento dell'interruzione", proprio come nell'ultimo esempio. Abbiamo anche un NGEarlyBreak con "orfani e vedove in violazione" (all'interno di #first
> prima di "riga 2"), che non è comunque perfetto, ma meglio di "violare la pausa evitare". Pertanto, faremo un a capo prima di "riga 2", violando la richiesta di righe orfane / vedove. La specifica tratta questo argomento nella sezione 4.4. Interruzioni forzate, che definisce quali regole di interruzione vengono ignorate per prime se non sono presenti breakpoint sufficienti per evitare l'overflow del frammentatore.
Conclusione
L'obiettivo funzionale del progetto di frammentazione dei blocchi LayoutNG era fornire l'implementazione a supporto dell'architettura LayoutNG di tutto ciò che supporta il motore legacy e il meno altro possibile, a parte le correzioni di bug. L'eccezione principale è il supporto migliorato per l'evitamento delle interruzioni (ad esempio break-before:avoid
), perché si tratta di un componente fondamentale del motore di frammentazione, quindi doveva essere presente fin dall'inizio, poiché l'aggiunta in un secondo momento avrebbe comportato un'altra riscrittura.
Ora che la frammentazione dei blocchi di LayoutNG è terminata, possiamo iniziare a lavorare all'aggiunta di nuove funzionalità, come il supporto di dimensioni di pagina miste durante la stampa, le caselle dei margini @page
durante la stampa, box-decoration-break:clone
e altro ancora. Come per LayoutNG in generale, prevediamo che la percentuale di bug e il carico di manutenzione del nuovo sistema diminuiranno notevolmente nel tempo.
Ringraziamenti
- Una Kravets per il piacevole "screenshot fatto a mano".
- Chris Harrelson per la correzione bozza, il feedback e i suggerimenti.
- Philip Jägenstedt per feedback e suggerimenti.
- Rachel Andrew per la modifica e la prima figura di esempio con più colonne.