Not blocking the UI in tight JavaScript loops

Everyone’s written a JavaScript loop that just loops over all the {LIs, links, divs} on a page\*, and it’s pretty standard. Something like var lis = document.getElementsByTagName(“li”); for (var i=0; i or, if you’re using jQuery: $(“li”).each(function() { // do something here to this });

This is problematic if there are, say, 2000 LI elements on the page, and what you’re doing in the loop is semi-intensive (imagine you’re creating a couple of extra elements to append to each of those LIs, or something like that). The reason this is a problem is that JavaScript is single-threaded. A tight loop like this hangs the browser until it’s finished, you get the “this script has been running for a long time” dialog, and the user interface doesn’t update while you’re in this kind of loop. You might think: aha, this will take a long time, so I’ll have some sort of a progress monitor thing: var lis = document.getElementsByTagName(“li”); for (var i=0; i but that doesn’t work. What happens is that the browser freezes until the loop finishes. Annoying, but there it is. One approach to getting around this is with timeouts rather than a `for` loop. var lis = document.getElementsByTagName(“li”); var counter = 0; function doWork() { // do something here to lis[i] counter += 1; progressMonitor.innerHTML = “processing list item ” + counter; if (counter < lis.length) { setTimeout(doWork, 1); } }; setTimeout(doWork, 1);

so you move the bit of work you need to do into a function, and that function re-schedules itself repeatedly, using `setTimeout`. This time, your user interface will indeed update, and your progress monitor will show where you’re up to. There are a couple of caveats with this: it’ll take a bit longer, and you’re no longer guaranteed to have things processed in the order you expect, but they’re minor issues. For doing this in jQuery, a tiny plugin: jQuery.eachCallback = function(arr, process, callback) { var cnt = 0; function work() { var item = arr[cnt]; process.apply(item); callback.apply(item, [cnt]); cnt += 1; if (cnt < arr.length) { setTimeout(work, 1); } } setTimeout(work, 1); }; jQuery.fn.eachCallback = function(process, callback) { var cnt = 0; var jq = this; function work() { var item = jq.get(cnt); process.apply(item); callback.apply(item, [cnt]); cnt += 1; if (cnt < jq.length) { setTimeout(work, 1); } } setTimeout(work, 1); };

and now you can do $.eachCallback(someArray, function() { // “this” is the array item, just like $.each }, function(loopcount) { // here you get to do some UI updating // loopcount is how far into the loop you are }); $(“li”).eachCallback(function() { // do something to this }, function(loopcount) { // update the UI }); Not always a useful technique, but when you need it, you need it.

More in the discussion (powered by webmentions)

  • (no mentions, yet.)