Supercharged Javascript and CSS animations

Apr 21st, 2015 in  by Michael Cho

Summary notes from the Browser Rendering Optimization course on Udacity focussed on building 60 fps apps.

Browser Rendering Optimization is an awesome Udacity course with lots of details about building websites with smooth scrolling and animation effects. It's a free course, so anybody who works with javascript and css should check it out.

I'm adding my notes below which are the main points only, but the course itself contains many more examples and insights not found here. You should definitely take a look at the course if you want a full understanding.


Lesson 1 - Understanding browser steps

1. Any scrolling or animations on any device requires the browser to render a new "frame".

2. Most devices today render at a speed of 60 frames per second (60 fps). 

3. In order to render 60 frames per second, this means each frame must be rendered in ~16 milliseconds. In reality, you have ~10-12 milliseconds since each browser has some housekeeping work.

4. If your application takes longer than this to render a frame, the browser will "skip" frames which causes the juddering in a poorly-optimised animation. (The course calls this "janking".)

5. Browsers go through steps for Javascript > Style > Layout > Paint > Composite. This happens with visual properties like changing heights / widths regardless of whether this is done with CSS or JS, and can happen multiple times per frame if your JS code keeps triggering style changes. 

6. See csstriggers.com for a full list of which properties trigger each of these steps.


Lesson 2 - Response, Animation, Idle, Load

1. Describes 4 main browser stages for any user interaction - RAIL - Response, Animation, Idle, Load. Alternatively named as LIAR.

2. Load - Try to get the initial load from the server to finish within 1 second, includes any critical functionality for interaction.

3. Idle - When the user is not actively interacting with the app, this is the perfect time to send other server requests. eg pre-load images and videos below the fold, comments, loader gifs, etc.

4. Response - Show some response to the user when he interacts with the app. For most simple cases (eg checkbox), this is fairly immediate and does not need to be optimized.

5. Animation - This is where a lot of the optimizations can speed up your app's interactions, so that it reaches the 60 fps target speed.

6. One technique shared to speed up the Animation was FLIP (First, Last, Invert, Play). This involves pre-calculating the endpoints of the animation, then applying that to the start position. The pre-calculation can be done *before* the user starts the interaction instead of *when* the user starts the interaction.


Lesson 3 - Developer Tools

1. In Chrome, Cmd-Alt-J will open developer tools (on a Mac). Use the Timeline tab, click record and interact with any website. Then you can pause recording and read the timeline.

2. The timeline will show which frames are exceeding the 60 fps requirement. You can then click each of these to see if there are any JS functions which can be optimized, usually by better use of loops and preventing re-painting of the frame.

3. To test on a mobile device, switch on developer mode on your Android device (Go to Settings > About Device, and tap Build Number 7 times). Turn on USB debugging (usually in developer options). Connect your device via USB, then on your desktop browser go to chrome://inspect/. This brings up your connected device. On your desktop chrome developer tools, you can also use "Screencast" to drive your mobile device from your connected desktop.

4. Debugging for iOS is more difficult, but possible with iOS WebKit Debug Proxy.


Lesson 4 - Javascript Optimizations

1. All browsers use Javascript engines to run your JS code, and they handle them slightly differently. The JS engine may not run your code exactly as you write it, so there is very little benefit performing "micro-optimizations" such as comparing between for(var i=0; i<len; i++) and while(++i < len).

2. requestAnimationFrame is a useful JS function to ensure that your animations do not interrupt the browser rendering, which will cause it to re-render. eg


function animate(){
    // Other stuff
    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

4. IE9 does not support requestAnimationFrame, but a polyfill can be used. 

5. Use Web Workers to do intensive processing, so that the main thread does not get blocked. eg


var imageWorker = new Worker('scripts/worker.js');

function manipulateImage(type) {
    // [snip] create a variable for imageData.... [/snip]
    imageWorker.postMessage({'imageData': imageData, 'type': type});

    imageWorker.onmessage = function(e){
        var image = e.data;
        // [snip] do something with the image... [/snip]
    }
}

Lesson 5 - CSS Optimizations

1. Where possible, use CSS selectors which reduce the number of elements the browser has to inspect. eg


// Fastest
.box--last > .title-container > .title

// Mid-speed 
div.box:not(:empty):last-of-type .title

// Slowest, since it has to calculate the number of n elements
.box:nth-last-child(-n+1) . title

2. Introduced the common problem of "Forced Synchronous Layout", when the browser has to first run a Layout step, then Style, and then Layout again (causing thrashing). Some examples:


// scrollY runs Layout, but changing the opacity then requires Layout to run again.
if (window.scrollY < 200) { elem.style.opacity = 0.5; }

// Similar here, offsetHeight runs Layout over and over
if (elem.offsetHeight < 500) { elem.style.maxHeight = '100vh'; }

// No Forced Synchronous Layout here since the offsetWidth is calculated 
first, then batch update the styles in the loop.
var newWidth = container.offsetWidth;
for(var i=0; i<len; i++) { elem.style.width = newWidth; }

3. As a general rule, read layout properties first and then batch any style changes. For example:


// change this
divs.forEach(function(elem, index, arr){
    if (elem.offsetHeight < 500) { elem.style.maxHeight = '100vh'; }
})

// to this
if (elem.offsetHeight < 500) { 
    divs.forEach(function(elem, index, arr){ elem.style.maxHeight = '100vh'; })
}

4. List of changes which trigger layouts


Lesson 6 - Layering

1. In Chrome Developer Tools > Timeline. Hit Escape to bring up the Details window, then check "Show paint rectangles" in the Rendering tab. This will highlight any sections of the page which is repainted in green.

2. If some element is being repainted very often, it is useful to move this to a separate _layer_ so that the whole page does not need to be repainted. Check "Show composited layer borders" in the Rendering tab to view layers which the browser has automatically set.

3. Use the will-change or translate css property to request the browser to set a new layer. eg 


.circle {
    transform: translateZ(0); // Used for Safari and older browsers
    will-change: transform; // Modern browsers
}

4. Browsers move elements to a new layer if there are any animations performed on it. To create a new layer on the fly is slower, so these css properties set the elements on a new layer to begin with.


Other articles you may like

Prioritized Code Review Checklist - what to look for first, second, and last
Sep 21st, 2017
Principles for Code Reviews
Sep 2nd, 2017
Running Metabase locally as a service
May 29th, 2017
Create a custom Alexa Skill with AWS Lambda - Part 3 (Lambda)
Dec 18th, 2016
Create a custom Alexa Skill with AWS Lambda - Part 2 (Alexa Skill)
Nov 10th, 2016