Jeremy Kahn's Dev Blog

There's always a method to my madness.

60 FPS or Bust: Dynamically Prerendering CSS Animations

Creating better animations for the web has been something I’ve been working on for a few years, and I’ve just figured out a new method that offers more flexibility and animation fidelity than was previously available. I call it “CSS prerendering,” and it’s easier than you think.

CSS vs. JavaScript animations

In the beginning, there was JavaScript. For years, JavaScript was the only way to add animation to a web page. As web standards matured, CSS become more featureful and offered transitions and animations. These browser-native CSS features are capable of delivering much smoother animation than JavaScript, because the browser seems to be able to optimize native animation. However, CSS animations and transitions are not terribly flexible, leaving developers to choose between a smooth, 60 frames per second animation, or an animation that can adapt to the needs of an application on the fly.

CSS animations (@keyframes) are inflexible because they must be hard-coded. Transitions are a little easier to work with, as they can gracefully respond to any CSS changes performed by JavaScript. However, the complexity that CSS transitions can give you is pretty limited — an animation with multiple steps is difficult to achieve. This is a problem that CSS @keyframe animations are meant to solve, but they don’t offer the level of dynamic responsiveness that transitions do.

CSS @keyframe animations are also limited by the native easing formulae available — there are only six. This is not enough to meet the needs of all designs. Joe Lambert came up with a way around this with Morf; a tool that generates a @-webkit-keyframe animation with many tiny steps. This opens the door to an unlimited selection of easing formulae. I decided to build on top of this idea with Rekapi’s Rekapi.DOMRenderer.toString feature, which uses a straightforward keyframe API to generate cross-browser compatible CSS code. I then used that functionality to create Stylie, which provides a UI to create animations and exports it to CSS code.

CSS prerendering

The idea behind CSS prerendering is pretty straightforward — you create a CSS @keyframe string at runtime with JavaScript, inject it into a dynamically-created <style> element, and then insert that element into the DOM. When the animation is complete, you simply remove the <style> element from the DOM. This approach works surprisingly well, particularly in WebKit-based browsers (other rendering engines still need to catch up).

Until now, there haven’t been any tools that I could find that can do this, so I decided to fill this void with Rekapi’s DOMRenderer module. Rekapi’s goal is to provide a usable and flexible API for creating keyframe animations, and historically it only performed JavaScript-based animations. With the DOMRenderer, you can use the same API to create a CSS @keyframe animation, making for magically smoother animations that don’t block the JavaScript thread. But don’t take my word for it, you can see the difference for yourself. The difference in quality is far more pronounced on mobile devices, especially iOS.

Creating a CSS animation with Rekapi

To create a CSS @keyframe animation that works in every browser, you need something like this (generated by Stylie):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
.stylie {
  -moz-animation-name: stylie-transform-keyframes;
  -moz-animation-duration: 2000ms;
  -moz-animation-delay: 0ms;
  -moz-animation-fill-mode: forwards;
  -moz-animation-iteration-count: infinite;
  -ms-animation-name: stylie-transform-keyframes;
  -ms-animation-duration: 2000ms;
  -ms-animation-delay: 0ms;
  -ms-animation-fill-mode: forwards;
  -ms-animation-iteration-count: infinite;
  -o-animation-name: stylie-transform-keyframes;
  -o-animation-duration: 2000ms;
  -o-animation-delay: 0ms;
  -o-animation-fill-mode: forwards;
  -o-animation-iteration-count: infinite;
  -webkit-animation-name: stylie-transform-keyframes;
  -webkit-animation-duration: 2000ms;
  -webkit-animation-delay: 0ms;
  -webkit-animation-fill-mode: forwards;
  -webkit-animation-iteration-count: infinite;
  animation-name: stylie-transform-keyframes;
  animation-duration: 2000ms;
  animation-delay: 0ms;
  animation-fill-mode: forwards;
  animation-iteration-count: infinite;
}
@-moz-keyframes stylie-transform-keyframes {
  0% {-moz-transform:translateX(0px) translateY(0px);}
  100% {-moz-transform:translateX(400px) translateY(0px);}
}
@-ms-keyframes stylie-transform-keyframes {
  0% {-ms-transform:translateX(0px) translateY(0px);}
  100% {-ms-transform:translateX(400px) translateY(0px);}
}
@-o-keyframes stylie-transform-keyframes {
  0% {-o-transform:translateX(0px) translateY(0px);}
  100% {-o-transform:translateX(400px) translateY(0px);}
}
@-webkit-keyframes stylie-transform-keyframes {
  0% {-webkit-transform:translateX(0px) translateY(0px);}
  100% {-webkit-transform:translateX(400px) translateY(0px);}
}
@keyframes stylie-transform-keyframes {
  0% {transform:translateX(0px) translateY(0px);}
  100% {transform:translateX(400px) translateY(0px);}
}

…And then paste that into your CSS code. That’s just for one, very simple animation. Here’s how that same animation might be set up with Rekapi’s DOMRenderer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var rekapi = new Rekapi(document.body);
var actor = new Rekapi.Actor(
    document.getElementsByClass('stylie')[0]);

rekapi.addActor(actor);
actor
  .keyframe(0, {
      transform: 'translateX(0px) translateY(0px)'});
  .keyframe(2000, {
      transform: 'translateX(400px) translateY(0px)'});

// Feature detect for @keyframe support
 if (rekapi.renderer.canAnimateWithCSS()) {
   rekapi.renderer.play();
 } else {
   rekapi.play();
 }

Rekapi handles all of the vendor prefixing for you, so you only need to write an animation once. And if the browser doesn’t support CSS animations, Rekapi will gracefully fall back to an old-fashioned JavaScript animation, which it has always supported. To see what kinds of complex animations Rekapi can handle, I encourage you to check out Stylie, which is simply a graphical front end for Rekapi.

Better animations

CSS lets developers and designers create fluid, efficient animations that don’t block the JavaScript thread. However, we have lacked the tools to make them easy to work with. I really want to solve this problem, and I think that Rekapi and its DOMRenderer significantly narrow the gap between performance and flexibility. I hope that CSS prerendering becomes more widely used, as it enables a much smoother web experience. I’d love to know how CSS prerendering works for you, and if you find any bugs, please report them. Happy animating!

Update: Rekapi has reached 1.0.0 after this post was originally published, and the API for CSS prerendering has changed somewhat. The article has been updated for accuracy.