Georgy Marcuk I'm a Frontend Developer with primary expertise in fine-tuned UX. I'm skilled in writing performant and team-maintainable code and passionate about accessibility, animation and "sweating the small stuff"

Using Animation Worklet

8 min read 2325


Houdini’s new Animation Worklet is part of a bigger initiative to bring native animations onto the web which also includes the Web Animations API (WAAPI).

Unlike other efforts we have seen lately, Animation Worklet brings some advantages to the game that we just couldn’t get in the past. This article explains what’s new with Worklets and where it can beat any current code or animation library.

Put simply, Animation Worklet (in a contrast to other animating techniques like CSS transition, CSS Animation with keyframes, or WAAPI) doesn’t have to be time-based. This allows us to base our animation on user input like the scroll, some previous state, or anything else we can access with JavaScript.

Worklet, being a lightweight worker, runs independently from the main JavaScript thread and aims at offloading the hard processing or frequent work outside of the main thread, so in an ideal world, any glitches or lags in the main thread do not have to result in a lag of the animation on the same page. Similarly, any bottlenecks in the animation don’t automatically result in slowed down overall experience of the page.


The syntax

Let’s dive into the code and see what it’s all about. The Worklet usage requires several steps of setup.

Prerequisites

Although the Animation Worklet is a new feature and the state of Worklet specification is a “working draft”, you can try to enable it now in Chrome under the Experimental Web Platform features flag. I recommend you do that if you’re going to try out the demos in this article.

Support check

Although it’s not recommended to use Worklet in production, it is still possible to use a Worklet with a fallback. Checking the support of Worklets will help with that. It will also prevent breaking the whole JavaScript execution.

if(!('animationWorklet' in CSS)) {
    document.write("Nope, no animationWorklet.");
}

Worklet animator

The animator, being a lightweight worker and serving as an extension of the browsers native functionality, needs to be registered as such. The way it is registered is confusing at first, but is pretty common in the latest features of browsers, like Service Workers. The animator is loaded from a separate file using CSS.animationWorklet.addModule() method. The method accepts the path to the animator file and returns a Promise to give us a callback for when our animator is loaded.

CSS.animationWorklet.addModule('./path/to/animator.js').then(function () {
  // we can use the animator here
});

The actual code to register the animator is pretty straightforward using the registerAnimator function. The function accepts two arguments, the name of the Worklet animator and the Worklet constructor in a form of a class.

The class can have methods like any other class, but the main method we are interested in is the animate method, which is used to reflect the changes to the current time to your animation. The animate method is called on every frame, and unlike requestAnimationFrame, the animation method is called as often as possible limited only by your hardware (the standard FPS value the requestAnimationFrame is trying to achieve is 60, while many monitors have higher frame rates).

registerAnimator('passthrough', class {
    animate(currentTime, effect) {
        effect.localTime = currentTime;
    }
});

The method accepts the currentTime and the effect object and is used to apply changes of a currentTime to the effect. currentTime is the value passed into a Worklet by the timeline, which, as I mentioned earlier, doesn’t have to be represented by a time when it comes to the Worklets. More on that later.

For now, let’s start with a simple example of letting the timeline time through and assign the same value to the effect object as an effect.localTime. The value of localTime property is then applied to the Worklet animation described further.

The Worklet

Once we have all of this setup, we can use the animator for our animation in the standard code. The animation is created with a WorkletAnimation constructor which accepts three arguments — the name of the animator, instance of a KeyframeEffect, and the timeline. A fourth option in the form of an object would be passed into the registerAnimator class constructor.

var worklet = new WorkletAnimation(
    'passthrough',  // name of animator
    new KeyframeEffect(
        document.querySelector('#box1'),
        [
            {
                transform: 'rotate(0)'
            },
            {
                transform: 'rotate(360deg)'
            }
        ],
        {  
            duration: 2000,
            iterations: Number.POSITIVE_INFINITY
        }
    ),
    document.timeline,  // timeline
    {}   // registerAnimator options
);
worklet.play();

The setup above will create a simple, infinite animation of a rotating box. By extending the keyframe object a bit more, we’ll achieve a box doing barrel rolls.

Note that the demos have been modified to use blobs instead of a separate file, to work in a Codepen. You can access the repository with the original demos on GitHub and GitHub Pages.

You’ll notice here that the syntax is straightforward and the options quite self-descriptive and easy to use. While that’s nice, there’s nothing here we can’t do with a simple CSS animation and keyframes.

Animator as a mathematical function

So, let’s try something more complex. The animator and its animate method can be anything we want it to be. That means we can apply mathematical functions to our timeline time…

registerAnimator('sin', class {
    animate(currentTime, effect) {
        effect.localTime = Math.abs( Math.sin( currentTime ) );
    }
});

…or some custom logic like rounding the number of a current timeline time.

registerAnimator('sin', class {
    animate(currentTime, effect) {
        effect.localTime = Math.round( currentTime * 100 ) / 100;
    }
});

This manipulation of timeline current values gives us a new way to run animations. Think of it as easing on steroids. Instead of having three or more points to describe the easing as a curve, we can use JavaScript to just program any behavior we want based on the current timeline value.

When modifying the animator with custom logic, it is important to keep in mind that effect.localTime is used as a current time of our animation timeline in the Worklet animation. That means that any negative numbers or calculations returning NaN cannot be processed into the animation. Naturally, our code should reflect that and implement precautions.

The scroll

The Animation Worklet is specifically designed to work with events like the scroll. It’s simple to base the animation timeline on the scroll, instead of time. All we need to do is use a ScrollTimeline constructor instead of classic document.timeline.

new WorkletAnimation(
    'passthrough',
    new KeyframeEffect(
        ...
    ),
    new ScrollTimeline({
        scrollSource: document.querySelector('#scroll'),
        orientation: "vertical", 
        timeRange: 1000
    })
).play();

There are some facts about the browsers that we need to know to understand why the Animation Worklet is worth using. In some cases, using the Animation Worklet can drastically improve performance.

Browser threads

Browsers are multithreaded. In fact, they have been for a while. However, that doesn’t automatically mean we get all our CPU cores and the GPU to process the web app when it’s running. Each browser actually takes a slightly different approach when implementing multithreading.

Chrome focuses on separating the browser tabs into isolated processes. This allows for each process/tab to run on a separate core and in some way utilize all the CPU when it’s working. It also has the advantage of only having one unresponsive tab when something goes wrong and the process gets stuck.

However, each process is running all the parts that normally could be reused by the webpage instances. This dramatically affects the amount of memory the Chrome needs to run, which impacts the battery life. But, anyone reading this and using Chrome probably already knows about this problem.

Firefox takes a different approach. It runs a maximum of four processes for all tabs. Each tab is assigned one of the processes to be in charge of the tab. With the Quantum update, it’s even able to process certain tasks for one tab in multiple threads. But that’s limited to tasks that can actually be separated into parallel threads, like the processing of websites CSS.

All in all, browsers are somewhat multithreaded, and they try to achieve as much process separation as possible. There is a certain limitation to that for all browsers.

Event Loop

Event Loop is something really essential to browsers, and yet, many people don’t know about it. Event Loop is in charge of scheduling and processing the tasks happening in the tab. No matter what, the task ends up in the queue and, from there, it’s executed when it’s time, even for asynchronous code. That’s why the main JavaScript thread is very hard to separate into multiple threads, because things are designed to happen one after another, not in parallel.

Frequent function calls

There is nothing wrong with running tasks in a serial manner in one thread until a frequent function call occurs that could block the thread. A perfect example of such event is the scroll.

Scroll event is something that is very often used in the application — whether it is to show/hide some kind of menu or simply transform some decorative parts of the web based on how far the user has scrolled. Some would even say that the scroll is the main interaction from the user and many web apps like Facebook, Instagram, or Twitter are primarily based on the user scrolling through the content.

That being said, any lagging of this interaction can be very annoying. Although it is an event we use on the web quite often, it is incredibly easy to mess up the performance. Running our code on the scroll can take a drastic part of our main browser thread, especially when it comes to expensive code like animation causing a re-render.

In fact, some browsers even started implementing the scroll listeners as passive by default (so the browser doesn’t have to wait for the execution to see if the event.preventDefault() appears in the code), to make the code more optimized unless specified otherwise.

That’s where the Animation Worklet comes to the rescue. As a native functionality offloading work to a separate thread, a browser can do all required calculations in a separate process and only get back with the rendering instructions. This is something that wasn’t possible before and could improve the animation based on scroll drastically. That is, of course, assuming the implementation of the browser will do the optimization correctly for us.

The state

registerAnimator is a class, and it comes with all the possibilities the class brings (and even some unexpected ones described later). The class can act independently, have a scope, save some kind of state, be extended, and more.

This means that the animation cannot only mirror the current time in the timeline, but also act based on the previous behavior or some state we have saved before. Following example changes the color of the box based on the direction of the scroll. The direction is detected by saving the previous scroll timeline state and manually setting the state of the timeline to the start/end based on the direction to affect the background of the box.

registerAnimator('state', class {
    constructor(options, state) {
        this.prevTime = 0;
    }

    animate(currentTime, effect) {
        if (this.prevTime > currentTime) {
            effect.localTime = 1;
        } else if (this.prevTime < currentTime) {
            effect.localTime = 1000;
        }
        this.prevTime = currentTime;
    }
});

Preserving state

The separation of the Worklet code into multiple parts has several reasons. One reason is to separate into independent threads. Again similar to Service Workers, the Animation Worklet can be terminated when not used and can save state across multiple reloads. Currently, we would have to use some other technology (like local storage) to achieve the same effect.

Unfortunately, even Chrome currently doesn’t support saving the state, or the documentation simply doesn’t reflect the recent changes of the API. But given that the feature is experimental, there is nothing wrong with that.

Conclusion

Animation Worklet is a promising addition to the native animation family. Its focus on separation of the workload onto other threads is definitely a good practice that we will see much more in the future implemented APIs.

Despite the neat functionality, there are still some parts around this technology that don’t quite make sense. For example, I find the naming convention regarding the timeRange option of the ScrollTimeline constructor confusing. It doesn’t really have anything to do with time.

Worklet is still clearly a work in progress, based on many factors, like the preservation of state, which doesn’t work as expected (or rather the documentation doesn’t state the current state of the API).

Something else worth mentioning is the openness of the API, like it was built to be extended. You can see this in a CSS.animationWorklet object, where the only currently implemented method is the addModule method used in this tutorial. The fact that applying the current time in the animator class is done by assigning a value to the effect.localTime suggests some more functionality to the effect object that is not yet implemented.

The new Worklet APIs includes things like PaintWorklet, AudioWorklet, or LayoutWorklet that will definitely bring some more interesting stuff, each focusing on solving a specific task. But let’s leave that for another time.

Plug: LogRocket, a DVR for web apps

https://logrocket.com/signup/

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.

Georgy Marcuk I'm a Frontend Developer with primary expertise in fine-tuned UX. I'm skilled in writing performant and team-maintainable code and passionate about accessibility, animation and "sweating the small stuff"

Leave a Reply