Web Runtime Performance

Luis Vieira
The UI files
Published in
6 min readNov 1, 2016

Many of todays performance concerns focus on getting assets to the user as fast as possible, page load time and time to first byte are often the most important and audited metrics, but runtime performance is many times forgotten and as important as these metrics.

It’s no good to have a optimized critical rendering path and a 1s time to first render, if then we provide an experience where scrolling feels slow and animations stutter, your website will still be perceived as slow and engagement will suffer.

Scrolling performance is crucial for content discovery, and fluid animations critical for a smooth experience and a good perception of speed.

Good principles for smooth animations

In order to make smooth 60fps animations you have to avoid making the browser go through all of the render pipeline each time you animate something.

Frame Render Pipeline from https://developers.google.com/web/fundamentals/performance/rendering/simplify-paint-complexity-and-reduce-paint-areas

Animate cheap properties

When we’re animating we want the browser to do as less work as possible, properties like ‘width’ and ‘height’ trigger layout, so this means that anytime you animate this property the browser has to recalculate the full layout of the page, this means each element position and size.

Also when we animate properties such as color or visibility, we’re triggering a paint this means that the browser has to re-render all the affected elements and the layers they belong to.

Animating in the compositor layer

In order to achieve 60fps smoothness in our animations we need to animate properties that can animate in the compositor layer

This is the render pipeline we want to achieve skipping layout and paint and animating directly in the compositor layer from https://developers.google.com/web/fundamentals/performance/rendering/simplify-paint-complexity-and-reduce-paint-areas

Compositor layer

By default the browser paints elements to a single layer in memory, you can think of it as a photoshop layer where all elements are placed there. In order to animate in a independent single layer while skipping layout and paint, we need to promote elements to their own layers.

element{
will-change: transform;
}
//or for older browsers
element{
transform: translateZ(0);
}

These rules will instruct the browser to promote the targeted element to it’s own compositor layer. Use this sparingly, layers consume memory and using them excessively may have unexpected results.

What can we animate then?
Not all css properties can be animated using the compositor layer, if you want to make high performance animations you’ll need to stick to these rules.

transform:translate()
transform:scale()
transform:rotate()
opacity

About Scroll

Scrolling is one of the most critical and common interactions in any website or application, that’s why it’s so critical that it remains smooth and jank free.

Everything starts with the DOM tree, which is essentially all the elements within the page. The browser takes a look at your styled DOM and it finds things that it thinks will look the same when you scroll. It then groups these elements together and takes a picture of them, which is called a layer. Each of these layers needs to be painted and rasterized to a texture and then composited together to the image that you see on screen.

Always debounce visual changes to the next requestAnimationFrame

A common function attached to scroll events is checking for elements in the page and the applying some css classes to them, this is a common pattern on parallax scrolling pages.

As scroll events fire at an high rate, as you check the position of each element in the DOM you’ll trigger a reflow, then as you atach css classes to those elements you’ll trigger a repaint.

A repaint occurs when changes are made to an elements skin that changes visibility, but do not affect its layout.

Examples of this include outline, visibility, or background color. According to Opera, repaint is expensive because the browser must verify the visibility of all other nodes in the DOM tree.

A reflow is even more critical to performance because it involves changes that affect the layout of a portion of the page (or the whole page).

/****
nasty scroll event function
***/
function onScroll() {
/****
Check for each element position in DOM
Check if element is in viewport
If true attach css class to element
*/
for(var i = 0; i < domElements.length; i++) {
//Check for each element position in DOM
var rect = domElements[i].getBoundingClientRect();
//Check if element is in viewport
if( rect.top >= 0 && rect.bottom <= window.innerHeight ){
//If true attach css class to element
domElements[i].classList.add('visible');
}
}
}
window.addEventListener('scroll', onScroll, false);

In the function above you can see that each time the scroll event fires we’re checking the position of a number of elements present in the DOM, and then making visual changes to those elements, triggering a reflow and repint each time. As the scroll event fires rapidly this will have a negative impact on sroll performance specially on less powerful devices.

This codepen is interesting to see the amount of times the scroll event fires and the amount of work we’re forcing the browser to do.

If you need to make any type of visual changes on scroll such as in a parallax website, it’s a good practice to do them on the next ‘requestAnimationFrame’.

var last_known_scroll_position = 0;
var ticking = false;

function doSomething(scroll_pos) {
// do something with the scroll position
}

window.addEventListener('scroll', function(e) {
last_known_scroll_position = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(last_known_scroll_position);
ticking = false;
});
}
ticking = true;
});

Understanding ‘requestAnimationFrame’

‘requestAnimationFrame’ is a new method available in the web platform that passes the responsibility of scheduling the animation to the browser, ensuring that the browser runs the animation before the next repaint making it easier to stay smooth at 60fps.

Do work when idle

If you need to do non esssential work such as sending analytics data, processing information that isn’t immediately needed by the user, or that doesn’t require immediate visual feedback, you can wait until the browser is idle, do this in a ‘requestIdleCallback’.

After input processing, rendering and compositing for a given frame has been completed, the user agent’s main thread often becomes idle until either: the next frame begins; another pending task becomes eligible to run; or user input is received. This specification provides a means to schedule execution of callbacks during this otherwise idle time via a `requestIdleCallback`

requestIdleCallback(myNonEssentialWork);function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
}
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
}
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();

if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
/*Check:
https://developers.google.com/web/updates/2015/08/using-requestidlecallback
*/

‘requestIdleCalback’ calls your function providing it with a ‘deadline’ object, that has a method ‘timeRemaining’ which returns how many time we have left to run our tasks. When this function returns zero we can schedule another function with requestIdleCallback

If the browser is overloaded with work there’s no guarantee that our callback will ever run, so we can pass a ‘options’ object with a ‘timeout’ property that defines the maximum amount of time that the browser will wait to run our callback.

requestIdleCallback(myNonEssentialWork, {timeout:2000});

Then you can update your function to:

function myNonEssentialWork (deadline) {

while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0)
doWorkIfNeeded();

if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}

The requestIdleCallback still hasn’t got much for browser support but there’s a shim available that basically falls back to ‘setTimeout’.

Sources:

--

--

Luis Vieira
The UI files

A frontend developer that can handle its dose of UX and design.