Preventing CSS transition blocking by JS code

CSS transitions are good for animating interface elements more smoothly than possible with pure Javascript animations, especially when combined with translate3d, which allows even simple 2d animations to be hardware accelerated. But it is still possible for CSS transitions to lag if JavaScript code is perform heavy processing while they are running.

For example, consider a panel sliding in from the right that will contain some content that must be loaded from the network. The animation will start out smooth, but once the content loads, the animation will stall as your code parses and formats the data, and the browser computes styles, reflows, and paints the new content. This is usually fast enough to be unnoticeable, but in cases where the processing is significant or the content structure is complex, it can noticeably lag the animation.

Here's a JSFiddle demonstrating the problem:

The core of the problem is that while the function processing the data is running, the browser is not allowed to do anything else, including update the animation, because of the single-threaded nature of the JavaScript interpreter. So we can solve it by telling the data processing function to yield to the browser every 15 milliseconds. This will ensure an animation FPS of 1000ms / 15ms = 60fps, assuming no other heavy processing is going on at the time.

The setTimeout function, which is normally used to schedule events in the future, can be used to perform such a yield. If the timeout is set to 0ms, the browser will call the provided function in the next cycle of its event loop, after processing all other event handlers, DOM updates, and animations.

We can wrap up this functionality into a simple function that will take a list of functions representing the work to be done, and call each in sequence until it reaches the 15ms limit, at which point it will yield before continuing:

function nonBlockLoop(fns){
    var d = new Date()
    while((new Date()) - d < 15){
        var f = fns.shift()
        if(!f) return;
    setTimeout(function(){nonBlockLoop(fns)}, 0);

This will require a slight modification to the original heavy processing function to break up the work into a number of functions, each of which will take less than 15ms to execute. In the case of loading data from the network and inserting it into the DOM, this is easy: simply make each item to add to the DOM a separate function.

Here's another JSFiddle showing the solution in action: