Part 2: CPU intensive javascript computations without blocking the single thread

October 24, 2015

Two years back I wrote a post about leveraging setTimeout to do a large task in javascript without locking the thread. The last few months I’ve gotten a couple of comments where people would like to see a more updated version using requestAnimationFrame instead of setTimeout.

Lets first work out the difference between the two. SetTimeout is a simple timer (just as setInterval), it takes two parameters, first a callback function and secondly an integer specifying how long to wait until the function callback should be fired. So if you set the integer to 10, the system will try to run the function after 10 ms (or if using setInterval, every 10ms).

But, the browser can’t realistically run a user defined function every 10 ms, and that’s because the browser itself has other things it needs to run to keep itself in sync. Inevitably you start to drift, sometimes you’ll render every 10 ms, and sometimes it might take 11 ms or 9 ms, etc. According to the html5 spec, you can specify a timing value down to 4 ms, but older browsers can have anything ranging from 10 ms to 50 ms, since it hasn’t been part of the spec prior to html5.

// Run this in your console to get an average of setInterval
var timer;
var startTime = new Date();
var timings = [];
var totalIterations = 10;
var timeout = 8;

// This timer will run 10 times, and at the end of it, console log the avg time between each call
// it should match or be quite close to the variable timeout.
timer = setInterval(
	function() {
		timings.push(new Date() - startTime);
		startTime = new Date();

		if(timings.length === totalIterations) {
			console.log('average time ' + (timings.reduce(function(a,b) {return a+b;})/timings.length) + 'ms');
			clearInterval(timer);
		}
	},
	timeout
);

So to solve the problem, requestAnimationFrame was introduced, which is basically the same thing as setTimeout(func, 16), in other words, a function that should run every 16ms (1000ms / 60 fps = 16.6ms). Since the function would start to drift at some point, the rendering would not be synced with the natural rendering state of the browser, and you’d end up with dropped frames (frames which were processed, but never rendered). The requestAnimationFrame is basically a hook to the internal rendering of the browser, stating, when you re-render, run this function as well.

In essence, the requestAnimationFrame is used in conjunction of you rendering something visible to the user (canvas drawing, updating DOM, etc). But we can use it for the purpose of looping through our dataset. It won’t necessarily be faster (4ms < 16.6ms), but it would be in sync with the browser, hopefully giving the single thread more time to process user interactions.

I’ve created a repository on github containing two classes and an example file. The first class, AsyncIterator, runs with requestAnimationFrame, and the second class, AsyncIteratorInterval, runs with setInterval. Through some non-scientific tests AsyncIteratorInterval runs just a bit faster.

The dataset is pretty simple, I’ve generated an array with 500,000 values sequentially [1…500000], and each value is then iterated through and multiplied by Math.PI to simulate some kind of workload. You can run the test for yourself here. I’ve run each test three times and calculated the average, these are the results that I’ve gotten on my computer:

TypeAvg time
For-loop9488 ms
AsyncIterator9846 ms
AsyncIteratorInterval (10 ms)9808 ms

Anyway, you might want to play around with the code. I’ve built the code so the asyncIterator and asyncIteratorInterval return a promise which will resolve once all items have been processed.

// The dataset
var myArr = [];
// Number of operations per call
var batchSize = 1000;
// The actual processing method
function work(item, index) {
	// Do something for each item
}

// Start iterator, it will return a promise
var promise = asyncIterator(myArr, work, batchSize);

// When promise is resolved, output results
promise.done(
    function(results){
        console.log('Done processing', results);
    }
);

Tags