Register for this year’s #ChromeDevSummit happening on Nov. 11-12 in San Francisco to learn about the latest features and tools coming to the Web. Request an invite on the Chrome Dev Summit 2019 website

Houdini's Animation Worklet

Supercharge your webapp's animations

TL;DR: Animation Worklet allows you to write imperative animations that run
at the device's native frame rate for that extra buttery jank-free smoothness™,
make your animations more resilient against main thread jank and are linkable
to scroll instead of time. Animation Worklet is in Chrome Canary (behind the
"Experimental Web Platform features" flag) and we are planning an Origin
Trial for Chrome 71. You can start using it as
a progressive enhancement today.

Another Animation API?

Actually no, it is an extension of what we already have, and with good reason!
Let's start at the beginning. If you want to animate any DOM element on the web
today, you have 2 ½ choices: CSS
Transitions for
simple A to B transitions, CSS
Animations for
potentially cyclical, more complex time-based animations and Web Animations
API
(WAAPI) for almost arbitrarily complex animations. WAAPI's support
matrix is looking pretty grim, but
it's on the way up. Until then, there is a
polyfill.

And what about statelessness? Think about Chrome's address bar on Android, for
example. If you scroll down, it scrolls out of view. But the
second you scroll up, it comes back, even if you are half way
down that page. The animation depends not only on scroll position, but also on
your previous scroll direction. It is stateful.

Another issue is styling scrollbars. They are notoriously unstylable — or at
least not styleable enough. What if I want a nyan cat as my
scrollbar?
Whatever technique you choose, building a custom scrollbar is neither
performant, nor easy.

The point is all of these things are awkward and hard to impossible to
implement efficiently. Most of them rely on events and/or
requestAnimationFrame, which might keep you at 60fps, even when your screen is
capable of running at 90fps, 120fps or higher and use a fraction of your
precious main thread frame budget.

Animation Worklet extends the capabilities of the web's animations stack to make
these kind of effects easier. Before we dive in, let's make sure we are
up-to-date on the basics of animations.

A primer on animations and timelines

WAAPI and Animation Worklet make extensive use of timelines to allow you to
orchestrate animations and effects in the way that you want. This section is a
quick refresher or introduction to timelines and how they work with animations.

Each document has document.timeline. It starts at 0 when the document is
created and counts the milliseconds since the document started existing. All of
a document's animations work relative to this timeline.

To make things a little more concrete, let's take a look at this WAAPI snippet

When we call animation.play(), the animation uses the timeline’s currentTime
as its start time. Our animation has a delay of 3000ms, meaning that the
animation will start (or become "active") when the timeline reaches startTime
+ 3000. After that time, the animation engine will animate
the given element from the first keyframe (translateX(0)), through all
intermediate keyframes (translateX(500px)) all the way to the last keyframe
(translateY(500px)) in exactly 2000ms, as prescribed by the duration
options. Since we have a duration of 2000ms, we will reach the middle keyframe
when the timeline's currentTime is startTime + 3000 + 1000 and the last
keyframe at startTime + 3000 + 2000. The point is, the
timeline controls where we are in our animation!

Once the animation has reached the last keyframe, it will jump back to the first
keyframe and start the next iteration of the animation. This process repeats a
total of 3 times since we set iterations: 3. If we wanted the animation to
never stop, we would write iterations: Number.POSITIVE_INFINITY. Here's the
result of the code
above.

Note: All demos currently require Canary with the "Experimental Web Platform
features" flag enabled on chrome://flags.

WAAPI is incredibly powerful and there are many more features in this API like
easing, start offsets, keyframe weightings and fill behavior that would blow the
scope of this article. If you would like to know more, I recommend reading this
article on CSS Animations on CSS
Tricks.

Writing an Animation Worklet

Now that we have the concept of timelines down, we can start looking at
Animation Worklet and how it allows you to mess with timelines! The Animation
Worklet API is not only based on WAAPI, but is — in the sense of the extensible
web — a lower-level primitive that
explains how WAAPI functions. In terms of syntax, they are incredibly similar:

The difference is in the first parameter, which is the name of the worklet
that drives this animation.

Feature detection

Chrome is the first browser to ship this feature, so you need to make sure your
code doesn't just expect AnimationWorklet to be there. So before loading the
worklet, we should detect if the user's browser has support for
AnimationWorklet with a simple check:

if('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}

Loading a worklet

Worklets are a new concept introduced by the Houdini task force to make many of
the new APIs easier to build and scale. We will cover the details of worklets a
bit more later, but for simplicity you can think of them as cheap and
lightweight threads (like workers) for now.

We need to make sure we have loaded a worklet with the name "passthrough",
before declaring the animation:

What is happening here? We are registering a class as an animator using the
AnimationWorklet's registerAnimator() call, giving it the name "passthrough".
It's the same name we used in the WorkletAnimation() constructor above. Once the
registration is complete, the promise returned by addModule() will resolve and
we can start creating animations using that worklet.

The animate() method of our instance will be called for every frame the
browser wants to render, passing the currentTime of the animation's timeline
as well as the effect that is currently being processed. We only have one
effect, the KeyframeEffect and we are using currentTime to set the effect's
localTime, hence why this animator is called "passthrough". With this code for
the worklet, the WAAPI and the AnimationWorklet above behave exactly the
same, as you can see in the
demo.

Master of time

The currentTime parameter of our animate() method is the currentTime of the
timeline we passed to the WorkletAnimation() constructor. In the previous
example, we just passed that time through to the effect. But since this is
JavaScript code, and we can distort time 💫

Note:currentTimecan be NaN in certain circumstances (more later). You
should keep that in mind when writing animation worklets. Since all
mathematical operations can handle NaN (they return NaN when one of their
inputs is NaN) we are fine here!

We are taking the Math.sin() of the currentTime, and remapping that value to
the range [0; 2000], which is the time range that our effect is defined for. Now
the animation looks very
different, without having
changed the keyframes or the animation's options. The worklet code can be
arbitrarily complex, and allows you to programmatically define which effects are
played in which order and to which extent.

Options over Options

You might want to reuse a worklet and change its numbers. For this reason the
WorkletAnimation constructor allows you pass an options object to the worklet:

Note: The options object will be structurally cloned when it is being sent to
the worklet, similar to how postMessage() operates.

In this example,
both animations are driven with the same code, but with different options.

Gimme your local state!

As I hinted at before, one of the key problems animation worklet aims to solve is
stateful animations. Animation worklets are allowed to hold state. However, one
of the core features of worklets is that they can be migrated to a different
thread or even be destroyed to save resources, which would also destroy their
state. To prevent state loss, animation worklet offers a hook that
is called before a worklet is destroyed that you can use to return a state
object. That object will be passed to the constructor when the worklet is
re-created. On initial creation, that parameter will be undefined.

Every time you refresh this
demo, you have a 50/50
chance in which direction the square will spin. If the browser were to tear down
the worklet and migrate it to a different thread, there would be another
Math.random() call on creation, which could cause a sudden change of
direction. To make sure that doesn't happen, we return the animations
randomly-chosen direction as state and use it in the constructor, if provided.

Note: The destroy() lifecycle hook has been replaced by getter method, but
this change is not reflected in the spec or Chrome’s implementation just yet.

Hooking into the space-time continuum: ScrollTimeline

As the previous section has shown, AnimationWorklet allows us to
programmatically define how advancing the timeline affects the effects of the
animation. But so far, our timeline has always been document.timeline, which
tracks time.

ScrollTimeline opens up new possibilities and allows you to drive animations
with scrolling instead of time. We are going to reuse our very first
"passthrough" worklet for this
demo:

Instead of passing document.timeline, we are creating a new ScrollTimeline.
You might have guessed it, ScrollTimeline doesn't use time but the
scrollSource's scroll position to set the currentTime in the worklet. Being
scrolled all the way to the top (or left) means currentTime = 0, while
being scrolled all the way to the bottom (or right) sets currentTime to
timeRange. If you scroll the box in this
demo, you can
control the position of the red box.

Note: It might look like you should be able to use ScrollTimeline with a
normal Animation, and we agree. This is planned, but currently not supported
in Chrome.

If you create a ScrollTimeline with an element that doesn't scroll, the
timeline's currentTime will be NaN. So especially with responsive design in
mind, you should always be prepared for NaN as your currentTime. It’s often
sensible to default to a value of 0.

Linking animations with scroll position is something that has long been sought,
but was never really achieved at this level of fidelity (apart from hacky
workarounds with CSS3D). Animation Worklet allows these effects to be
implemented in a straightforward way while being highly performant. For example:
a parallax scrolling effect like this
demo shows that it
now takes just a couple of lines to define a scroll-driven animation.

Under the hood

Worklets

Worklets are JavaScript contexts with an isolated scope and a very small API
surface. The small API surface allows more aggressive optimization from the
browser, especially on low-end devices. Additionally, worklets are not bound to
a specific event loop, but can moved between threads as necessary. This is
especially important for AnimationWorklet.

Compositor NSync

You might know that certain CSS properties are fast to animate, while others are
not. Some properties just need some work on the GPU to be animated, while others
force the browser to re-layout the entire document. Sites like
CSSTriggers.com show you which properties are fast
to animate, and which are not.

In Chrome (as in many other browsers) we have a process called the compositor,
whose job it is — and I'm very much simplifying here — to arrange layers and
textures and then utilize the GPU to update the screen as regularly as possible,
ideally as fast as the screen can update (typically 60Hz). Depending on which
CSS properties are being animated, the browser might just need have the
compositor do it's work, while other properties need to run layout, which is an
operation that only the main thread can do. Depending on which properties you
are planning to animate, your animation worklet will either be bound to the main
thread or run in a separate thread in sync with the compositor.

Note: You should avoid "slow" properties at all costs. Limit yourself to
animation opacity and transform to make sure your animations run smoothly
even on slow devices.

Slap on the wrist

There is usually only one compositor process which is potentially shared across
multiple tabs, as the GPU is a highly-contended resource. If the compositor gets
somehow blocked, the entire browser grinds to a halt and becomes unresponsive to
user input. This needs to be avoided at all costs. So what happens if your
worklet cannot deliver the data the compositor needs in time for the frame to be
rendered?

If this happens the worklet is allowed — per spec — to "slip". It falls behind
the compositor, and the compositor is allowed to re-use the last frame's data to
keep the frame rate up. Visually, this will look like jank, but the big
difference is that the browser is still responsive to user input.

Note: This is what the spec allows the browser to do. Chrome does not currently
do any of these things, but will implement these behaviors soon™

Conclusion

There are many facets to AnimationWorklet and the benefits it brings to the web.
The obvious benefits are more control over animations and new ways to drive
animations to bring a new level of visual fidelity to the web. But the APIs
design also allows you to make your app more resilient to jank while getting
access to all the new goodness at the same time.

Animation Worklet is in Canary and we are aiming for an Origin Trial with
Chrome 71. We are eagerly awaiting your great new web experiences and hearing
about what we can improve. There is also a
polyfill
that gives you the same API, but doesn't provide the performance isolation.

Keep in mind that CSS Transitions and CSS Animations are still valid
options and can be much simpler for basic animations. But if you need to go
fancy, AnimationWorklet has your back!

rss_feed
Subscribe to our
RSS or
Atom feed
and get the latest updates in your favorite feed reader!