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.
Let’s dive into the code and see what it’s all about. The Worklet usage requires several steps of setup.
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.
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.
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).
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.
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.
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.
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…
…or some custom logic like rounding the number of a current timeline time.
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 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.
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.
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.
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.
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.
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.
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
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.