How Chromium Optimizes Scrolling Off the Main Thread
This article provides an overview of how the Chromium web browser achieves smooth, lag-free scrolling by moving scroll processing away from the main thread. By delegating input handling and visual updates to a dedicated compositor thread, Chromium prevents heavy JavaScript execution and complex layout calculations from freezing the user interface. We will explore the limitations of the main thread, the architecture of the compositor thread, the use of GPU layerization, and how passive event listeners ensure uninterrupted scrolling performance.
The Main Thread Bottleneck
In a traditional browser architecture, the main thread is responsible for almost everything: parsing HTML, calculating CSS styles, building the Document Object Model (DOM), executing JavaScript, and painting pixels.
When a user scrolls, the browser must update the visual viewport. If the main thread is busy executing a heavy JavaScript file or recalculating a complex layout, it cannot process the scroll input immediately. This delay results in "jank"—a stuttering, unresponsive visual experience where the page freezes instead of gliding smoothly.
Enter the Compositor Thread
To solve the main thread bottleneck, Chromium splits the rendering process across multiple threads, most notably introducing the compositor thread. The compositor thread operates independently of the main thread and acts as the first responder for user input.
When you scroll a webpage in Chromium, the operating system sends that input event to the browser process, which immediately routes it to the compositor thread—not the main thread. Because the compositor thread does not run JavaScript or perform layout calculations, it is almost always free to process the scroll input instantly.
Layerization and the GPU
For the compositor thread to scroll a page without the main thread's help, it needs pre-rendered visual data. Chromium achieves this through a process called layerization.
- Breaking Down the Page: The main thread divides the
webpage into separate, overlapping layers based on CSS properties (like
transform,opacity, orposition: fixed) and scrolling containers. - Tiling and Rasterization: Because a webpage can be much larger than the screen, the compositor breaks these layers into smaller squares called tiles. Worker threads then convert these tiles into bitmaps (rasterization) and upload them to the GPU's memory as textures.
- Drawing the Frame: When a scroll event occurs, the compositor thread simply instructs the GPU to shift the position of these pre-existing textures and draw a new frame.
Because the content is already in the GPU's memory, moving it up or down requires zero involvement from the main thread, allowing Chromium to maintain a consistent 60 to 120 frames per second, even if the main thread is completely frozen.
Handling JavaScript and Passive Event Listeners
The biggest obstacle to off-main-thread scrolling is JavaScript. Web
developers can attach wheel or touchstart
event listeners to the page and call
event.preventDefault(), which cancels the browser's default
scrolling behavior.
If a page has these listeners, the compositor thread cannot scroll immediately. It must wait for the main thread to run the JavaScript to see if the scroll will be canceled. Chromium mitigates this delay in two ways:
- Non-Fast Scrollable Regions: The compositor keeps a map of areas on the page that have blocking event listeners. If the user scrolls outside these regions, the compositor scrolls immediately. If the user scrolls inside one, the compositor waits for the main thread.
- Passive Event Listeners: Chromium introduced the
{ passive: true }flag for event listeners, and eventually made it the default for document-level touch and wheel events. A passive listener tells the browser that the JavaScript will never callpreventDefault(). This guarantee allows the compositor thread to scroll the page immediately while the main thread executes the JavaScript in the background.
Scroll Unification
In recent years, Chromium completed an architectural upgrade known as "Scroll Unification." Previously, if a scroll was too complex or involved specific main-thread dependencies, the browser would fall back to handling the entire scroll on the main thread.
With Scroll Unification, the compositor thread handles 100% of all scroll gestures. Even in rare cases where the compositor must briefly ask the main thread which element is being targeted (hit-testing), the actual movement of the scrollbar and viewport remains strictly controlled by the compositor thread, ensuring a unified and consistently fast scrolling pipeline.