Going jank free - Achieving 60 FPS smooth websites
I love small details in web design. Little geometric shapes, lines and scribbles in the background or layered through the content; it's a nice way to add visual flair without taking up space for content.
How about some animations when scrolling and navigating through a page? Subtle animations can draw the user's attention, provide visual clues and can be used for visual storytelling.
Web technology has been rapidly gaining new features to easily create creative and visually impressive multimedia experiences, all in your browser!
What is "jank"?
Most device screens are 60 Hz, which indicates that the screen refreshes the visible image 60 times a second. In the development world, we talk about frames per second (FPS) instead of Hz, and we call the property "refresh rate". Software that runs on these devices (like browsers) usually try to match the refresh rate of the screen as closely as possible. So when an animation or transition is running, the browser tries to put 60 new pictures (or frames) per second on the screen.
If we calculate (1000 ms / 60 FPS) the time each frame has to be rendered, we'll see that a frame only has a total time of 16.6 ms. In reality the duration is actually closer to 10ms per frame, since browsers have some housekeeping work to do behind the scenes. When the browser fails to render in that duration, the framerate drops lower than the refresh rate of the screen. This results in judder of the page content on screen. This is also referred to as jank, and it negatively impacts the user experience.
1. The pixel pipeline
When changing a "layout" property of an element in a page, the browser will have to check all the other elements and update the layout, or "reflow" the page. Areas on the page that have been affected will have to be repainted and composited back together. Properties that trigger a reflow change the element's geometry (
position, etc.). For example, reflows can be forced by changing a
display property, appending an element to the document or animating the element's size or position.
2. Writing performant CSS for animation
Looking at the pixel pipeline, you can tell that CSS has effect on the style and layout steps. Whenever you update a property that affects the flow of the page, the browser will reflow the page. This is performance-costly, but modern browsers are smart enough to only paint the updated area, not the entire page.
Animating paint and compositing properties
It's a good idea to minimize the amount of reflows the browser has to do during an animation. You might think that a combination of
position: absolute and changing the
left properties won't affect the surrounding elements, but that's not correct. For example, elements can have a percentage value for
width, which it will derive from the parent element. Also using units like
vw are environment variables that will cause reflow.
Every element's geometry properties (i.e.
top) are handled by the CPU in the first place. For each frame in the animation the geometry of the element will be recalculated and the trigger a reflow, then the updated area gets drawn (paint step in pipeline). Next, the drawn area has to be stitched back together by the compositing step.
To optimize the compositing, the browser has to ensure that the animated CSS property:
- does not affect the document's flow,
- does not depend on the document's flow,
- does not cause a repaint.
Forcing GPU acceleration
CSS animations aren't always handled by the GPU. To force GPU accelerated rendering you can use one of the many classic (hacky) methods in CSS like:
transform: translateZ(0) or
transform: translate3d(0, 0, 0).
What the browser does is cache the current drawn image of the element in GPU memory and handle all rendering by GPU. So in the pixel pipeline we are moving all the work to the compositing step from there on. Be aware that by changing an element's layout or paint property like
border, the browser will have to reflow and repaint the affected area. So use this hack wisely!
Still, if you really insist on writing your own, here's some things to consider.
Leveraging CSS solutions
In the example above, the element with the letter "A" inside is being animated by CSS. The only thing clicking on the button does is toggle a class on the element.
RequestAnimationFrame and timers
When working with animations in JS, it's best to use
requestAnimationFrame() or rAF for short. rAF does not guarantee that your animation will be smooth or 60 FPS, but it tries to avoid frame loss and is more efficient than
When a browser tab is inactive, rAF will pause the animation by blocking requestAnimationFrame callbacks which will preserve animation state.
rAF is non-deterministic, which means that we don't know when exactly it will get called. That's why we are forced to use time to keep track of the animation progress. If we'd purely use rAF, the animation could last 1 second (60 FPS), or 2 seconds (30 FPS) or more, depending on the refresh rate of your browser and the system work load.
An example with element "A" being animated by CSS and element "B" being animated by JS with
Throttling and debouncing (scroll)events
mouseMove events are a little tricky to get performant. Having the events call a function for each time they're triggered is a surefire way to drain your system's resources.
What we need to do is throttle and debounce the callback of the event.
By implementing throttling and debouncing in the event handler, we reduce the amount of times the callback is fired. Any animation that relies on the callback is now much less resource-heavy.
4. Use DevTools to find performance issues
The developer toolbar is your best way to find and eliminate optimization issues. There are a couple of tools and options in Chrome DevTools that might come in handy when you're unsure what is causing the jank.
The FPS meter is a good way to get a quick glance of what is happening on the screen, performance-wise. Enable it through the command palette (⌘ + ⇧ + P) while the DevTools panel is open, and search for "Show frames per second (FPS) meter". Or enable it through: Customize and control DevTools menu (⋮) › More tools › Rendering › FPS meter. You can interact with a 3D model of the composited layers, check the memory usage per layer and see the layer is composited.
With the performance tool in Chrome, you can record runtime performance data. Pressing record, you can interact with the website while performance metrics are being captured. After stopping the recording, the data gets processed and the results will be shown in the performance panel.
You'll get hit in the face with an enormous amount of data, so knowing how to read and analyze the data is important.
Whenever you're optimizing for the compositing step, it is a good idea to check the Layers tool. Open the command palette and search for "Layers" or go to Customize and control DevTools menu (⋮) › More tools › Layers.
You can read all about how to use these tools and how to analyze the data here: Performance Analysis Reference.
Smooth animations on the web are not a given. When you see a smoothly animated website, you know that there's a lot of time and effort put into the details. When a device has a refresh rate of 60 Hz, the browser will aim for 60 frames per second rendering. The developer has influence on some crucial steps in the pixel pipeline and can optimize the time the browser takes to render each frame. Being aware of the pixel pipeline gives you a clearer way to make UX and UI choices around optimization.
As a front-end developer you're always striving to create something beautiful and eye-catching but it should not distract the user from reaching their their own goals on the website. Janky animations are something you want to avoid not only because they can be jarring and confusing for the user, they also lower the experience quality of your product.