FLIP Your Animations
Animations in your web app should run at 60fps. Not always easy to achieve that, and it really depends on what you're trying to do, but I'm here to help. With FLIP.
Update 17th May, 2022 - The lovely folks at GSAP have made their FLIP Plugin freely available - check it out!
Recently I've had the pleasure of being part of the team that built the Google I/O 2015 website, and last year I built the Chrome Dev Summit site. On both sites we used FLIP, which is essentially a principle, and not a framework or a library. It is a way of thinking about animations, and attempting to keep them as cheap as possible for the browser which, all being well, should translate over to 60fps animations.
If you prefer watching to reading, this is my talk from Chrome Dev Summit, where I explain FLIP (without naming it explicitly) in lots of detail:
The general approach #
What we're trying and do is to turn animations on their head (flip, see? Sorry) and, instead of animating “straight ahead” and potentially doing expensive calculations on every single frame we precalculate the animation dynamically and let it play out cheaply.
FLIP stands for First, Last, Invert, Play.
Let's break it down:
- First: the initial state of the element(s) involved in the transition.
- Last: the final state of the element(s).
- Invert: here's the fun bit. You figure out from the first and last how the element has changed, so - say - its width, height, opacity. Next you apply
transform
s andopacity
changes to reverse, or invert, them. If the element has moved 90px down between First and Last, you would apply a transform of -90px in Y. This makes the elements appear as though they're still in the First position but, crucially, they're not. - Play: switch on transitions for any of the properties you changed, and then remove the inversion changes. Because the element or elements are in their final position removing the transforms and opacities will ease them from their faux First position, out to the Last position.
Ta daaaaa!
Got Code? #
Why yes I do. Here's that same breakdown in code:
// Get the first position.
var first = el.getBoundingClientRect();
// Now set the element to the last position.
el.classList.add('totes-at-the-end');
// Read again. This forces a sync
// layout, so be careful.
var last = el.getBoundingClientRect();
// You can do this for other computed
// styles as well, if needed. Just be
// sure to stick to compositor-only
// props like transform and opacity
// where possible.
var invert = first.top - last.top;
// Invert.
el.style.transform =
`translateY(${invert}px)`;
// Wait for the next frame so we
// know all the style changes have
// taken hold.
requestAnimationFrame(function() {
// Switch on animations.
el.classList.add('animate-on-transforms');
// GO GO GOOOOOO!
el.style.transform = '';
});
// Capture the end with transitionend
el.addEventListener('transitionend',
tidyUpAnimations);
However, you can also do this with the upcoming Web Animations API, which can make things even easier:
// Get the first position.
var first = el.getBoundingClientRect();
// Move it to the end.
el.classList.add('totes-at-the-end');
// Get the last position.
var last = el.getBoundingClientRect();
// Invert.
var invert = first.top - last.top;
// Go from the inverted position to last.
var player = el.animate([
{ transform: `translateY(${invert}px)` },
{ transform: 'translateY(0)' }
], {
duration: 300,
easing: 'cubic-bezier(0,0,0.32,1)',
});
// Do any tidy up at the end
// of the animation.
player.addEventListener('finish',
tidyUpAnimations);
Right now you'll need the Web Animations API polyfill to use the code above, though, but it's pretty lightweight and does make life a lot easier!
If you want a more “in-production” context for FLIP check out the GSAP FLIP Plugin.
What is it good for? #
There is a window of 100ms after someone interacts with your site where you're able to do work without them noticing.
It's absolutely superb for times where you are responding to user input and then animating something in response. So, for example, in the case of Chrome Dev Summit, I was expanding cards that the user tapped on. Often the start and end locations and dimensions of the elements aren't known, because - say - the site is responsive and things move around. This will help you because it's explicitly measuring elements and giving you the correct values at runtime.
The reason you can afford to do this relatively expensive precalculation is because there is a window of 100ms after someone interacts with your site where you're able to do work without them noticing. If you're inside that window users will feel like the site responded instantly! It's only when things are moving that you need to maintain 60fps.
We can use that window of time to do all that getBoundingClientRect
work (or getComputedStyle
if that's your poison) in JavaScript, and from there we make sure that we're reducing the animation down to nice-and-fast, compositor-friendly, look-ma-no-paints transform
and opacity
changes. (Why just those? Check out my Pixels are Expensive post.)
Animations that can be remapped to transform and opacity changes are the perfect fit.
Animations that can be remapped to transform
and opacity
changes are the perfect fit. If you're already limiting to those properties in your JS or CSS animations then you're probably good to go; this technique works best when you're changing layout properties (like width and height, left and top) and you want to remap them to cheaper properties.
Sometimes you will need to rethink your animations to fit this model, and on many occasions I've separated and animated elements individually just so that I can animate them without distortion, and FLIP as much as possible. You may feel like that's overkill, but to me it's not, and for two reasons:
- People want this. My colleague and friend Paul Kinlan recently ran a survey on what people want from a news app. The most popular answer (which was a surprise to him, at least) wasn't offline support, sync, notifications, or anything like that. It was smooth navigation. Smooth, like no jank, no stutter, no judder. (/me mutters something about #perfmatters.)
- Native app devs do this. Of course this is a sliding scale and subjective, but I've heard many times of native developers spending days on end getting the transitions in their apps just right. Those “little touches” are a differentiator and, as we get faster loading sites through Service Worker, we're going to be in the same boat. People will judge our sites based on how well they feel when they're running.
Some caveats #
There are a couple of things to bear in mind if you FLIP:
- Don't exceed the 100ms window. It's important to remember that you shouldn't exceed that window, because your app will appear non-responsive if you do. Keep an eye on it through DevTools to know if you're busting that budget.
- Orchestrate your animations carefully. Imagine, if you will, that you're running one of these animations all transformy and opacity-y and then you decide to do another, which requires a bunch of precalculation. That's going to interrupt the animation that's in flight, which is bad. The key here is to make sure your precalculation work is done in idle or the “response window” I talked about, and that two animations don't stomp over each other.
- Content can get distorted. When you're working in a scale and transform world some elements can get distorted. As I said above I've been known to restructure my markup a little to allow me to FLIP without distortion, but it can end up being quite the wrangle.
FLIP on, as it were... #
I've come to love FLIP as a way of thinking about animations, because it's a good match of JavaScript and CSS. Calculate in JavaScript, but let CSS handle the animations for you. You don't have to use CSS to do the animations, though, you could just as easily use the Web Animations API or JavaScript itself, whatever's easiest. The main point is that you're reducing the per-frame complexity and cost (which normally means transform
and opacity
) to try and give the user the best possible experience.
I have more to tell you about FLIP and a broader, unified model of performance, but that's going to be the next blog post or so I'd say!