Saturday, May 24, 2014

Parallax Done Right

Getting great performance with parallax is tough. Here's how to make it happen.

Tldr; There are an easy handful of things you can do to get buttery parallax scroll. Check out a demo I cooked up to see it in action.
Parallax has become, for better or worse, an increasingly popular web trend. The first parallax site I ever saw was Ian Coyle's BetterWorld for Nike. I was in love. At the time, I'd never seen the technique before. It felt like I'd left the web of PDF-esque static pages and entered the future.

Since then, parallax has blown up. It feels like every day there's a new marketing page using the technique…and with good reason. Done right, it feels awesome. The problem is, a vast majority of sites using parallax suffer from terrible scroll performance. It's especially bad on devices with high pixel density like retina MacBook Pro's.

I've played quite a bit with parallax sites and have come up with a non-comprehensive list of Do's and Don'ts that will hopefully keep you on track to get killer performance.

Before we go through the commandments of parallax though, I would encourage you to check out the demo if you haven't already.

Some Do's

Only use properties that are cheap for browsers to animate. Those are, more or less: translate3d, scale, rotation and opacity. Anything else and you're probably not going to be running at 60fps.

Use window.requestAnimationFrame when firing the animations in JS. This basically tells the browser animate stuff before the next repaint. Do this instead of just directly adjusting properties.

window.requestAnimationFrame(animateElements);

Round values appropriately. If you're animating some object 100px over the course of 200px worth of scroll (so an object at 50% normal speed), don't let it get pixel values like 54.2356345234578px. Round that to nearest pixel. Trying to do opacity? Two decimal places will likely do.

animationValue = +animationValue.toFixed(2)

Only animate elements in viewport. Continuing to pass thousands of values during scroll to elements off-screen makes no sense and can be expensive.

Any code example I tried for this felt super contrived. Probably best to check out the living demo code to understand one way to do this.

Animate only absolutely and fixed position elements. I'm not 100% why, but I've seen significant performance pickup with only animating absolute/fixed elements. As soon as I apply a single animation to a relative/static element, the fps suffer.

.parallaxing-element { position: fixed; }

Use natural <body> scroll. Some browsers, specifically Safari I've noticed, really take a performance hit on scrolling elements other than the body. Honestly, I can't think of a good reason to do it. Even in cases when everything on the page is positioned fixed, so there's no actual scroll height, just use JS to set an appropriate body height to get the height of scroll you need for all your parallaxing goodness.

Define all your animations in an object, not as messy spaghetti madness. This has almost nothing to do with performance, but it just makes everything so much easier. Plus, c'mon, you're worth it.

Some Don'ts

Avoid background-size:cover unless you're sure it's not affecting performance. It's usually fine if you're not animating that element, but as soon as you try to translate it, there's a chance it'll cause serious problems. If you must have a full-bleed background that parallaxes, try other techniques to "full-bleed" the image.

Don't bind directly to scroll event. Use an interval to update element positions. The scroll is called like a bajillion times a second and can cause crazy performance hiccups. Instead, for elements that are parallaxing, just update their position every 10ms or so.

scrollIntervalID = setInterval(animateStuff, 10);

Don't animate massive images or dramatically resize them. Forcing browsers to resize images (especially huge ones) can be very costly. Now, that's not to say using scale on an image element is bad — in fact in my experience it seems to work quite well — but resizing a 4000px wide image to be 500px wide makes no sense and it just expensive all around.

Avoid animating 100 things at once if you're seeing performance problems. Honestly, I've not run into the issue of moving too many elements at once (even when animating upwards to 15 things simultaneously), but I'm positive it can happen. I have seen occasional performance problems when animating a parent and child at the same time though — avoid it if you can.

Quick Points on the Demo

How much of a difference can these handful of rules make? A huge difference. Not following even a couple of these can make the difference between butter and more chop than a Bruce Lee movie.

It should be noted that the demo:

is a bit contrived and simple.

is far from perfect in terms of organization and feature-completeness.

completely neglects fallbacks and mobile.

can get stuck if you scroll like a maniac because I haven't implemented guards against (though this wouldn't be particularly tough).

hasn't been extensively tested on different machines since I'm traveling and only have access to my MacBook Pro and m'lady's Air. Update: Someone on the internet's mentioned Windows machines might be choppy…I promise I'll fix when I'm done traveling!

Better yet, check out Dropbox's marketing site for Carousel. They follow basically all these rules, have been so gracious as so not obfuscate their JS, and the code is pretty easy to understand. The polish on the site is quite incredible. They have a touch version and have even gone so far as to ease their scrolling implementation, resulting in a somehow liquid feel (not necessary for 60fps, but an interesting and nice touch). It should also be noted that I learned a couple of these above rules from studying their site — huge shoutout to @destroytoday who implemented it!

Final Notes

Here are a few final tidbits that might be of value if you're considering diving into parallax development.

Use the Chrome Inspector to checkout Timeline > Frames to record some FPS action, or just navigate to about:flags in Chrome and turn on the FPS counter (though I prefer Frames in Inspector since it can go above 60fps).

It's totally a design-touch, but easing values instead of just regurgitating linear ones goes a looooong way in making parallax feel right. Look for the easeInOutQuad function in the JS if you're interested.

Be aware, this is just one technique for parallax. Another really performant (arguably more performant) technique is to use canvas, though I shy away from it because of the added complexity. That said, it's totally viable. This technique is used right here on Medium.

Remember, not every browser/device is going to be able to handle parallax. Make sensible fallbacks for touch devices, smaller viewports and even potentially older browsers. Again, I ignored this in the demo... cause it's just a demo.

Finally, while I'm all for parallax sites, I would encourage you to ask yourself if the parallax you're implementing is adding value or just "cause it's cool". Either way is fine, but just know adding parallax can add significant complexity.