What is fragmentation?
The CSS 2.1 specification defines a box model to represent the layout of a document and pretty much everything is a box. Normal flow nodes (e.g. not absolutely positioned) are laid out child by child starting at the top of their parent element box. If an element’s box is too small to fit all the content, the content is said to overflow and this overflow can either be visible or get clipped.
Fragmentation is different from overflow because it allows flowing the content through multiple boxes called fragmentation containers – fragmentainers for short. When the end of the current fragmentainer is reached a break occurs and the layout continues within the next fragmentainer. Using CSS, authors can also force breaks to occur after or before an element and even avoid them altogether.
The important detail to remember is that fragmentation doesn’t mean taking the overflow of a box and visually moving it to another one. Fragmentation happens during the layout and affects the dimensions of the content boxes.
There are a few specifications in CSS based on fragmentation:
- CSS3 Pagination - the document is laid out so it can fit on pages; the pages act as fragmentainers.
- CSS3 Multi-column - the element defines a number of columns that fill its box and where the content is laid out; the columns act as fragmentainers.
- CSS3 Regions - selected content forms a flow that’s laid out inside boxes called regions; the regions act as fragmentainers.
All of these specifications share common concepts covered by the CSS3 Fragmentation specification.
Fragmentation in WebKit
The Layout Process
To fully understand the concepts presented here you should know a bit about how layout works in WebKit. There are some nice articles on the Web covering the basics such as Bem’s article.
Long story short, the DOM tree is internally mapped to a render tree that is used to build up the box properties for every node. The render tree is made up from objects like
RenderInline etc. that represent the concepts defined in CSS2.1. During layout this tree is traversed and the various geometrical properties of the renderers are computed based on the style values. In the end all the elements have their box information computed.
As a side note, you’ll notice the symbols inside WebKit are usually named using the “pagination” or “page” terminology. This is for historical reasons, but most of the time the pagination concepts map to the fragmentation ones – pages, columns or regions.
There were a couple of ways the fragmentation behavior could have been implemented in WebKit. One of them was to have a renderer for each element fragment. For instance a simple paragraph with ten lines where there’s a break after the fourth one, we would have had one renderer with four lines in the first fragmentainer and a second renderer with six lines in the second fragmentainer. Both of the renderers would belong to the initial element. This approach is difficult to implement correctly and maintain because of the complexity it brings to the codebase. Additionally, it is also very risky from a security standpoint as it can introduce many subtle memory management bugs.
A rule of maximum one renderer per DOM node was created because of these concerns. Fragmentation is implemented by shifting the monolithic boxes (boxes that can’t be fragmented, such as line boxes – rectangles wrapping each line of text) so that they don’t overlap with the breaks. The correct rendering is obtained during the painting phase by placing each fragment exactly where it is supposed to appear in the fragmentainer. More about this topic in the next section.
In the case of unforced breaks, a position adjustment, called pagination strut in the codebase, is attached to the boxes during layout. This value represents a shift offset from the default layout position to the next fragmentainer in case the box doesn’t fit the current one. This offset needs to be stored separately on each box, not as a part of the top position because it’s not an attribute of the renderer. It’s a layout artifice that helps measuring the fragmented boxes. For example, if a block is collapsing at the top margin with its container (e.g. it’s the first child) and it also has a pagination strut, that offset will be transferred to the container. This makes sense because both the block and the container need to be placed in the next fragmentainer even though it’s the child that won’t fit.
The same rule applies to the first line box of a block. Also, during subsequent layouts it’s possible for an element to shift in the block-flow direction. The line boxes pagination struts need to be recomputed because the shift offsets are most likely different after the element changed position.
As an example, in the case of the example above, the first four line boxes would be positioned normally in the first fragmentainer. The fifth line box may need to have a pagination strut defined if it fits only partially in the first fragmentainer. This pagination strut would logically shift the line inside the next fragmentainer. The rest of the lines are all placed normally in the second fragmentainer.
In the case of forced breaks, the pagination strut is no longer used because the authors directly specify where the breaks occur. The boxes are positioned inside their container so they respect the break condition.
Each type of fragmentation layout has its own specialized behavior. The multi-column elements have the
ColumnInfo object attached to them. It contains information about the number and the width of the columns, the distance between breaks (used to balance the columns if they have auto-height) etc. This object is also pushed on the layout state stack so it can be accessed from any point inside the render tree.
The regions implementation is currently the most complex type of layout that makes use of fragmentation. The content node renderers are not attached to their DOM parent renderer. Instead, they are moved to a special object,
RenderFlowThread, that sets up a pagination context when it is laid out. These renderers need to hold and use various information about the flow thread: the regions size, how the descendant boxes change width in every region they are flowing into etc. Because the concept of the flow thread is so generic there are plans to port the multi-column implementation on top of it.
The Layout Performance
From a performance perspective, the layout process will always be slower for fragmented content because the engine can’t apply the same optimizations that are made for continuous vertical content. For example, without the possibility of fragmentation, updating the top margin of an element can shift it on the vertical axis but no relayout may be needed. If the element is enclosed in a fragmentation context, by shifting the element it’s possible it won’t fit any more inside the fragmentainer, triggering a break. To cover this case, a relayout of the element is always required.
The layout engine is usually optimized for both speed and memory consumption. This means fragmentation code is used only when necessary: if there is a printing context, if the node being laid out is a part of a multi-column element or if the renderer belongs to a flow thread. The logic for enabling the fragmentation code can be found inside the
LayoutState class. During layout, a stack of
LayoutState objects is created and stored on the root of the render tree, the
RenderView. Any renderer can query the top of this stack to determine if the content can be fragmented or not.
The Painting Phase
The painting of the renderers in WebKit is handled by the
RenderLayer tree. For some renderers the engine creates layers that are used to paint the document in the correct order (e.g. with respect the z-index restrictions). Layers are always created for the renderers that fragment, such as multi-column blocks or
RenderFlowThread objects. Because of this, multi-column elements and regions create stacking contexts and are painted as a single item.
For regions, when the painting operation occurs, the engine takes into account the fragmentation properties of the renderer and shifts the layer to the correct position so the painting is always executed inside the correct fragmentainer. The content not belonging to the current fragmentainer is clipped so the engine always paints only the content that should be displayed in a certain area. The mechanism is in some ways similar to how sprites (e.g. CSS Sprites) work.
For example, let’s say the paragraph above is flown into regions A and B. When region A is painted, the flow thread layer is called to paint in the fragmentainer box. It will paint the first four lines and then blank space until the bottom of the fragmentainer because the fifth line was shifted at layout-time using the pagination strut. When region B is painted, the flow thread layer is called again to paint inside the fragmentainer box, but at a new offset, where the region B fragment starts (top of the fifth line). The last six lines of the paragraph are painted.
Something similar happens with the multi-column blocks. However, the main difference is the fragmentainers for multi-column blocks don’t have their own layers. By using the same layer as the multi-column block the content is able to use advanced graphics features such as 3D transforms and video tags (see WebKit Compositing for more details about how accelerated layers work).
In the case of a flow thread, making its layer work with the region layers is one of the major challenges that need to be solved. As a consequence of this problem, content layers that require accelerated compositing (e.g. video layers) will not work correctly inside flow threads.
The modern fragmentation concepts are not yet fully implemented inside WebKit. There are some issues with the handling of forced breaks and the
avoid value is implemented only for the
break-inside property. The
orphans properties were just recently fully enabled.
On the CSS Regions side there’s some work left to do to achieve a smooth integration with the layers and compositing subsystem. Testing is also much needed to ensure good integration with other Web features.