Web Platform Team Blog

Adobe

decor

Making the web awesome

CSS Fragmentation In WebKit

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.

Paragraph in regions.

Example of a paragraph flown in two regions.

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 RenderBox, RenderBlock, 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.

When a line doesn't fit the current fragmentainer it is shifted using the pagination strut.

When a line doesn’t fit the current fragmentainer it is shifted using the pagination strut.

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.

The RenderFlowThread is painted in the two regions at the offsets corresponding to each content fragment.

The RenderFlowThread is painted in the two regions at the offsets corresponding to each content fragment.

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.

Conclusions

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 widows and 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.

8 Comments

  1. March 05, 2013 at 11:28 am, Thierry Koblentz said:

    I thought fragmentainers created new block-formatting contexts but the pics on this page seem to suggest otherwise. It looks like the margin on the paragraph collapse.
    In a multi-column construct for example, we’d have the margin contained inside the parent box, correct? Or is that not common to fragmentainers?

    • March 05, 2013 at 11:42 am, stearns said:

      Thierry – some fragment containers (like CSS Regions) do create a BFC, but others (like columns) do not. The multicolumn element creates a BFC, but its child column boxes do not.
      I’m seeing some paragraph margin in the pictures – the space between the blue fragment container line and the gray paragraph line (it’s a single paragraph being fragmented).

  2. March 05, 2013 at 11:59 am, Thierry Koblentz said:

    Stearns – May be we’re saying the same thing, but not seeing the same thing ;)
    If we take the first image in this article, I would expect to see the same space at the top of region A as the one I see at the bottom of region B. In both cases, I’d assume that these are due to margin containment (because the parent of the paragraph creates a BFC).

    As a side note, there is something wrong with this page. It is sometimes unresponsive which makes commenting kinda ‘painful’.

    • March 05, 2013 at 1:10 pm, Alan Stearns said:

      Thierry – if Region B were an auto-height region then I agree, there would be the same amount of space. But if Region B has a specific height, then the space could differ from the paragraph’s bottom margin. Since Region A has a specific height (causing the break) I was assuming Region B did as well.

      • March 05, 2013 at 1:31 pm, Thierry Koblentz said:

        Alan – the height of these boxes should have nothing to do with the space at the top of Region A, isn’t? To simplify: if the image shows no space at the top of Region A then it means the margin on the paragraph collapses, hence the Region does not create a new BFC. But I thought it would/should(?), hence my original comment.

        • March 05, 2013 at 1:45 pm, Alan Stearns said:

          You are correct – Regions do create a BFC, and the margin above the paragraph would not collapse. I was interpreting the tiny gap between the gray and blue lines as the margin, but this doesn’t match the default paragraph margins you’d normally see. I agree that the diagrams should have used default paragraph margins (it’s the middle of the night for Andrei, so it will be a while before we get a response from him).

        • March 06, 2013 at 1:49 am, Andrei Bucur said:

          Hi Thierry!

          Sorry if the images are misleading! :( I’ve intentionally truncated the paragraph margins so the examples are more compact. This helps me emphasise more the fragmentation concepts. Also, the white space at the bottom of the region B appears because the region is taller than the paragraph fragment. The bottom margin of the paragraph has nothing to do with it.

          Hope this helps.

          • March 06, 2013 at 8:20 am, Thierry Koblentz said:

            Hi Andrei – I think for many people images like these are the short version (TL;DR) of the article. For example, I’m learning from you and Alan that Region B doesn’t have an auto-height ;-)

            In any case, this has nothing to do with my initial comment which relates to margin collapsing.
            I’d think “compact” is not as good as “accurate”. It may help you “emphasize more the fragmentation concepts”, but something is lost in translation as we all agree that failing to display white-space at the top of Region A suggests that regions do not create new block-formatting contexts.