breadcrumbs:
author: wiltzius@
To understand the symptoms and potential causes of jank, it‘s important to have a basic understanding of the browser’s rendering pipeline. This article will briefly describe that pipeline assuming knowledge of the web platform but no knowledge of how the rendering engine works, with links to relevant design docs for more info.
This article is adapted from a similar section in Using Frame Viewer to Bust Jank. It goes into greater detail, but doesn't cover any examples. See that document for a more illustrated version of this process, which annotates a trace of an example style modification.
We'll focus on how the page is rendered after page load -- that is, how the browser handles screen updates in response to user input and JavaScript changes to the DOM. Rendering when loading a page is slightly different, as compromises to visual completeness are often made in order to get some content to appear faster; this is out of scope for this document which will focus on interactive rendering performance.
Much more detail about the specifics of how rendering inside of Blink works (i.e. the style, layout, and Blink paint systems), as well as a (somewhat dated) discussion of how it might evolve, can be found in the Rendering Pipeline doc. This document is intended to be more descriptive, higher-level, and include the related input and compositing systems.
After loading, the page changes in response to two inputs: user interaction mediated by the user agent (default pressed button styles, scrolling, pinch/zooming, etc) and updates to the DOM made by JavaScript. From there, a cascade of effects through several rendering sub-systems eventually produces new pixels on the user’s screen.
The main subsystems are:
Input handling \[technically not related to graphics per se, but nevertheless critical for many interactions\] Parsing, which turns a chunk of HTML into DOM nodes Style, which resolves CSS onto the DOM Layout, which figures out where DOM elements end up relative to one another Paint setup, sometimes referred to as recording, which converts styled DOM elements into a display list (SkPicture) of drawing commands to paint later, on a per-layer basis Painting, which converts the SkPicture of drawing commands into pixels in a bitmap somewhere (either in system memory or on the GPU if using Ganesh) Compositing, which assembles all layers into a final screen image and, less obviously, the presentation infrastructure responsible for actually pushing a new frame to the OS (i.e. the browser UI and its compositor, the GPU process which actually communicates with the GL driver, etc).
As an example, updating the innerHTML of a DOM element that’s visible on-screen will trigger work in every one of the systems above. In bad cases any one of them can cost tens of milliseconds (or more), meaning every one of them can be considered a liability for staying within frame budget -- that is, any one of these stages can be responsible for dropping frames and causing jank.
The browser has multiple threads, and because of the dependencies implied in the above pipeline which thread an operation runs on matters a lot when it comes to identifying performance bottlenecks. Style, layout, and some paint setup operations run on the renderer’s main thread, which is the same place that JavaScript runs. Parsing of new HTML from the network gets in own thread (in most cases), as does compositing and painting; however parsing of e.g. innerHTML is currently performed synchronously on the main thread.
Keep in mind that JavaScript is single threaded and does not yield by default, which means that JavaScript events, style, and layout operations will block one another. That means if e.g. the style system is running, Javascript can’t receive new events. More obviously, it also means that other JavaScript events (such as a timer firing, or an XHR success callback, etc) will block JS events that may be very timely -- like touch events. NB this essentially means applications responding to touch input must consider the JavaScript main thread the application’s UI thread, and not block it for more than a few milliseconds.
The ordering of these events is often unfortunate, since currently Blink has a simple FIFO queue for all event types. The Blink Scheduler project is seeking to add a better notion of priorities to this event queue. The scheduler will never be able to actually shorten non-yielding events, though, so is only part of the solution.
It's worth noting here that scrolling is “special” in that it happens outside of the main JavaScript context, and scroll positions are asynchronously reported back to the main thread and updated in the DOM. This prevents janky scrolling even if the page executes a lot of JavaScript, but it can get in the way if the page is well-behaved and wants to respond to scrolling.
It's also worth noting that the pipeline above isn’t strictly hierarchically ordered -- that is, sometimes you can update style without needing to re-lay-out anything and without needing to repaint anything. A key example is CSS transforms, which don’t alter layout and hence can skip directly from style resolution (e.g. changing the transform offset) to compositing if the necessary content is already painted. The below sections note which stages are optional. Different types of screen updates require exercising different stages in the pipeline. One way to keep animations inexpensive is to avoid stages of this pipeline altogether. The following sections cover a few common examples.
Lastly, note that user interactions that the browser handles, rather than the application, are largely unexceptional: clicking a button changes its style (adding pseudo-classes) and those style changes need to be resolved and later stages of the pipeline run (e.g. the depressed button state needs to be painted, and then the page needs to be re-composited with that new painted content).
Updating the viewport's scroll position has been designed to be as cheap as possible, relying heavily on the compositing step and its associated GPU machinery.
In the simple case, when the user tries to scroll a page events flow through the browser as follows:
This flow skips a lot of detail, which is covered in the Accelerated Compositing design doc if you're curious.
Important notes:
Modifying the style of existing DOM elements from JavaScript (whether in response to input or not) will exercise most of the rendering pipeline, although it'll skip parsing. Depending on the styles modified, it may also skip layout and painting. Paul Irish and Paul Lewis have written a good article on html5rocks covering which styles affect which pipeline stages.
Here's an outline of how modifying style propagates through the browser:
Important notes:
This flow summarizes a lot of detail, again covered in the Accelerated Compositing design doc.
Adding new DOM or replacing a DOM subtree requires parsing the new DOM. Whether this parsing happens synchronously on the main thread or asynchronously off-thread depends on how the DOM is inserted. An incomplete listing:
After parsing, rendering proceeds roughly as above in “modifying style of existing DOM”