Eliminating Choppy Animations by Leveraging the Browser Compositor Thread<!-- --> | <!-- -->Web Performance Tips

Eliminating Choppy Animations by Leveraging the Browser Compositor Thread

Many web applications utilize animations to achieve a variety of visual effects. For example, a web application may surface an animated splash screen while booting or display a loading animation while a UX module is loading.

For an animation to appear smooth and consistent to the human eye, it must be consistently produce frames at 60 frames per second. When an animation is unable to consistently produce frames at this rate, it will appear choppy and slow.

See the comparison below (source example here).

In this tip, I'll discuss why animations can manifest as choppy, and how an animation can be authored such that it is consistently producing frames at 60 frames per second via Compositor Thread acceleration.

Root Causing Choppy Animations

Typically, if a web application produces choppy animations, it's likely that the animation mechanism itself is reliant on the Main Thread. As many modern web applications utilize extensive client JavaScript to operate, the animation mechanism is competing with heavy JavaScript-related Tasks, such as Parse, Compilation, and Execution.

In the profiler, a choppy animation will often manifest like this:

A screenshot of the Profiler during a choppy animation. Highlighted is a note about Partially Presented Frames.

Notably, the browser profiler will show a warning about Partially Presented Frame or Dropped Frame. This means the browser was trying to produce a frame, but was unable to. It'll manifest as a large period of no produced frames in the Frames pane, and it's often accompanied by large JavaScript Tasks occurring at the same time.

As with all Main Thread bound user interface updates, the browser's frame rendering mechanism runs intermittently. When an animation is dependent on the Main Thread, its frame rate becomes bound to the cadence at which the Main Thread can re-run the Render Steps.

Fortunately, the browser provides a mechanism to accelerate animations: offloading them to the Compositor Thread!

Is my Animation Mechanism Compositor Thread Compatible?

If your animation is manifesting as choppy, the answer is likely No.

As of writing, this is my general rubric for determining if an animation is able to be accelerated by the Compositor Thread.

Animation MechanismCompositor Accelerated
CSS Animations (non-Layout affecting properties)Yes ✅
CSS Animations (Layout affecting properties)No ❌
Web Animations API (non-Layout affecting properties)Yes ✅
Web Animations API (Layout affecting properties)No ❌
SVGNo ❌
requestAnimationFrameNo ❌
setInterval, setTimeoutNo ❌
Lottie.jsNo ❌

Browsers are constantly evolving and this list may not always be up to date. If in doubt, collect a trace!

Layout Affecting Properties

Note: animating Layout affecting properties means that the animation is specified to animate properties that require access to Main Thread only information (like the Layout Tree), and therefore must be bound to the Main Thread.

Examples of Main Thread bound Layout properties include like width and height:

.button:hover {
    /* DANGER! This will be bound by the Main Thread! */
    transition: width 1s ease-in-out;
}

Preferred properties include transform, opacity, or others that don't require Layout information:

.button:hover {
    /*
     * This is able to be Compositor accelerated!
     * transform does not require data from Main Thread.
     */
    transition: transform 1s ease-in-out;
}

How does Compositor Acceleration work?

The Compositor Thread resides within the Renderer Process (where your web application executes). It's responsible for orchestrating graphical operations, receiving information from the Main Thread and coordinating with the browser's pixel rendering and graphics process (Raster and Display). It's a separate thread available for web applications to utilize, but many web developers are unaware of its existence!

Unlike the Main Thread, the Compositor Thread does not run JavaScript, and web developers cannot access it imperatively through any API (like Web Workers). However, you can notify the browser that you'd like to offload certain graphical operations to the Compositor Thread by correctly and carefully declaring your animation.

A diagram showing the Main Thread sending a message with Rendering Info to the Compositor Thread

When an animation is properly offloaded to the Compositor Thread, it can consistently produce frames uninterrupted from Main Thread activity (like heavy JavaScript). This can have a tremendous impact on user experience, especially during the phases of the application lifecycle that require intensive JavaScript (i.e. client-side React Rendering, or parsing and compiling large JavaScript bundles).

A diagram showing the animation running on the Compositor Thread

Examples

Let's consider a few examples, and explore how each of these could be accelerated via the Compositor.

The live example can be found here.

requestAnimationFrame

In this example, an animation is driven through requestAnimationFrame. It explicitly relies on the Main Thread because it requires JavaScript to execute. While requestAnimationFrame does try to execute once per frame, it is not guaranteed a consistent rate of 60 Frames per second if long tasks occur:

let previousTimestamp;
const animationDurationInMs = 2000; // A full rotation should take place over 2 seconds.
const animate = (timestamp) => {
    if (!previousTimestamp) {
        previousTimestamp = timestamp;
    } else {
        let timeDifference = timestamp - previousTimestamp;
        timeDifference %= animationDurationInMs;

        const rotationAmount = Math.round(360 * (timeDifference / animationDurationInMs));

        const node = document.getElementById('js-animation');
        node.style.transform = `rotate(${rotationAmount}deg)`;
    }
    requestAnimationFrame(animate);
};

requestAnimationFrame(animate);

When the Main Thread is not too busy, this will animate smoothly at 60 frames per second:

A diagram showing the animation running on the Main Thread smoothly producing frames

However, if the Main Thread experiences a Long Task, it will be unable to produce frames consistently:

A diagram showing the animation running on the Main Thread unable to timely produce frames

CSS Animation

Consider the following CSS instead of the above mechanism:

@keyframes spin-forever {
    100% {
        transform: rotate(360deg)
    }
}

.css-animation {
    animation: spin-forever 2s linear infinite;
}

The browser will understand that any visually rendered elements with the .css-animation applied should animate on the Compositor Thread.

In effect, regardless of any heavy JavaScript on the Main Thread, the animation will be accelerated and running unaffected on the Compositor Thread. Other updates from the Main Thread (such as other elements being rendered via client-side React rendering) may arrive to the Compositor thread, but it will not affect the cadence of the accelerated CSS animation.

A diagram showing the animation running on the Main Thread unable to timely produce frames

SVGs

Although SVGs surface a declarative animation syntax, they are not always guaranteed to be accelerated by the Compositor Thread.

<svg
  width="300px"
  height="300px"
  viewBox="0 0 100 100"
  xmlns="http://www.w3.org/2000/svg">
  <rect x="25" y="25" width="50%" height="50%" style="transform-origin: center" fill="darkblue">
    <animateTransform
      attributeName="transform"
      attributeType="XML"
      type="rotate"
      from="0 0 0"
      to="360 0 0"
      dur="2s"
      repeatCount="indefinite" />
  </rect>
</svg>

Many UX designers use Design tools that produce SVG animations. When UX Designers create SVG animations, they are not executing alongside heavy JavaScript like an actual browser environment. When an exported SVG from one of these tools is integrated into a production frontend, it may unexpectedly appear slow and choppy! This is because SVG based animations are not accelerated via the Compositor.

For example: <animateTransform> in SVG, although declarative, does not signal to the browser to move the animation to the Compositor thread and still relies on Main Thread data structures (similar to the requestAnimationFrame example).

Conclusion

Choppy animations can degrade user experience. By properly declaring animations in a way that leverages the browser's compositor thread, you can ensure your animations are consistently smooth and uninterrupted by Main Thread activity.

A special thanks to Alex Russell and Sabrina Vega who helped distill this behavior during a performance exploration.

That's all for this tip! Thanks for reading! Discover more similar tips matching Browser Internals.