Thursday 17 May 2012
Accelerated Scrolling In Firefox: Past, Present And Future
Scrolling performance is always important and we've made some pretty huge strides since Firefox 4. It may be helpful for me to explain some of what's happened.
In Firefox 3.6 and earlier, we had some pretty complicated code to analyze every scroll operation and figure out if we could safely scroll by just blitting some region of the window to another place and repainting the rest of the affected area. (In the very old days this was done in nsViewManager; later it was done using frame display lists.) This was quite difficult to do; for example an HTML <textarea> has a solid white background behind the scrolled text, so to prove that you can just blit the text, you have to determine that there is a solid-color background underneath the entire scrolled area. But the real problem was that it would quite often fail and we'd end up repainting the entire scrolled area, or most of it. For example, a large position:fixed element would require us to repaint the area around it. A background-attachment:fixed background would require us to repaint the entire window. This was bad because it meant scrolling sometimes "fell off a performance cliff".
In Firefox 4 we overhauled scrolling to use the new layer system. The basic idea is very simple: while scrolling, you put the content that's moving into one set of layers, and the content that's not moving in other layers. To scroll, add a translation to the transform matrix for the moving layers, repaint any parts of layers that have scrolled into view, and recomposite the window. If we do that properly, we'll never have to fall back to the "repaint everything" path; scrolling will just "always be fast". It also integrates cleanly when scrolled content uses accelerated layers for <video> and <canvas> etc.
This being the Web, there are of course a lot of complications. Separating moving from non-moving content is not easy. The infamously complex CSS z-ordering rules mean that even when scrolling a single element, you can have moving content interleaved with non-moving content so that you have to have two or more layers for each.
When you place content in these separate layers, the layers can become difficult to render. In particular text using subpixel-antialiasing placed into a moving layer, where its background is not moving, needs to be stored with separate alpha channels for each color channel ("component alpha"). This is difficult to implement efficiently. With GPU-based compositing we use shader tricks, but with CPU compositing it would be too expensive so when we encounter this situation we back off and stop caching the text in a retained layer and just draw it every time we composite the window. However even with CPU compositing, we still win on a lot of pages that use position:fixed or background-attachment:fixed.
Another thing that makes layer-accelerated scrolling difficult is when scrolled content is subject to some effect from its ancestors. The most common difficult case is when an element combines non-'visible' 'overflow' with 'border-radius' and must clip its descendants to a rounded-corner rectangle. The best way to handle this is to add support to the layer system so a container layer can clip its descendants to the rounded-rectangle border-box of an element. Until recently we didn't have that, so scrolling inside such elements was forced to repaint everything, but Nick Cameron just landed a large project to add that capability to layers and to use it in all kinds of rendering situations, including when scrolling. That means in a scrolling element that's clipping to its border-radius, the scrolled content is rendered to retained layer buffer(s) as if there was no border-radius, and then we clip to the border-radius during compositing (very efficient with a GPU since we cache the border-radius shape in a mask texture). (Nick's project provides other benefits such as accelerated video and canvas inside containers clipping to their 'border-radius' without hitting nasty fallback paths.) Summary: in Firefox 15, scrolling inside an overflow:hidden/auto element with border-radius gets a lot faster.
There is of course more to do. There are still CSS and SVG container effects we don't handle: namely filters, masks, and clip-path. Masks and filters need more layer-system support (especially filters!). Once those are done, then at least with GPU acceleration enabled it will be very difficult to hit any slow "repaint everything" paths when scrolling. (Although it's already very rare to see scrolling content inside a clip-path, mask or filter.)
The other pending important scrolling improvement is asynchronous scrolling, i.e., the ability to trigger a smooth scroll and have it happen immediately at a constant frame rate without jerkiness, even if the thread running the Web content is blocked due to Javascript execution or whatever. We've already developed most of this for Mobile Firefox (and B2G), but it needs to be made to work on desktop as well, which is not trivial. It requires enabling asynchronous compositing on all our platforms, and teaching the compositor a bit about scrolling. Once that's done, because we're able to layer-ize scrolling in almost every situation, we'll be in extremely good shape.
Comments