This is as days pass by, by Stuart Langridge

And this is Not blocking the UI in tight JavaScript loops, written , and concerning JavaScript and the DOM

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<lis.length; i++) { // yes this could be more efficient, don't care
  // do something here to lis[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<lis.length; i++) { // yes this could be more efficient, don't care
  // do something here to lis[i]
  progressMonitor.innerHTML = "processing list item " + i; // fail
};
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.

Comments

Justin Haygood

I actually pioneered the use of this method in the Flex apps we have at my employer (which, being based on Flash and ActionScript, has the same problem). We call it the "async" loop and it works quite well. It actually improved overall performance in many cases (since a freezing environment actually slowed things down for some reason)

Allan Bogh

Wouldn't it be easier to handle this function in a new Javascript thread? You can use AJAX to call a webpage and when the XMLHttpRequest object's onreadystatechange property fires the first time you can perform your function. Now you've officially forced your function into a new thread and let the page load.

xmlhttp.onreadystatechange=function()

{

if(xmlhttp.readyState==1) //we just care that it's initialized

{

dowork();

}

}

another Stuart

this might not be relevant, but before you add children to an element you should remove it from the DOM. That way the browser won't redraw. You might also be able to remove the ul/ol parent elements before doing anything in order to save even more redraws.

Reflective Perspective - Chris Alcock » The Morning Brew #382

[...] Not blocking the UI in tight JavaScript loops - Stuart Langridge looks at a few techniques you can use to avoid locking up your users browser when your JavaScript is performing looping operations [...]

fforw

If the reason for the timing problems is that you are modifying each li you find, it might also be a good idea to try to create a new ul/ol and add copies to that new list and then replace the old list with the new one when you're finished in one operation.

mrben

Ooooo - this might prove very useful in a script I have that "folds" a load of text into sections with mootools - can take ages to load and it does hang the browser, which is a bit annoying.

alexander farkas

There is a little jQuery-Plugin for this already. You can find it here: http://plugins.jquery.com/files/lazyEach.js.txt

Tom Berger

Yes, that's a nice solution (of course you have to pay attention to all sorts of concurrency-related problems that arise, but what can you do). Browser implementations of JS could really benefit from a way to explicitly hand control back to the event loop. It's totally doable. Somethine like:

for (i=0; i<99999; i++) {

doStuff();

window.doEvents();

}

Exc

You could probably use DOM workers too, depending on circumstances. No?

https://developer.mozilla.org/Using_DOM_workers

Dao

"it’ll take a bit longer,"

It'll actually take significantly longer, because the timeout won't actually be one ms. It will be normalized to something like 20 ms, depending on what the browser's minimum is.

"and you’re no longer guaranteed to have things processed in the order you expect"

Each step will be initiated by the previous step, so the order is in fact guaranteed.

"You could probably use DOM workers too"

DOM workers -- I'm not sure why they are called like that -- can't access DOM nodes.

Paul

A quick optimisation of this technique to avoid excessive overhead from setTimeout would be to divide the work into discrete steps so if you have 2000 elements to inject, do it in 20 calls to setTimeout and process 100 items at a time.

Rakesh Pai

Allan: Wouldn’t it be easier to handle this function in a new Javascript thread?

JavaScript doesn't support threads (well, not until HTML5 at least, and even then you can't access the DOM from that thread.)

Allan: Now you’ve officially forced your function into a new thread and let the page load.

No, you haven't. You've only queued up execution, much like a setTimeout, since there's only one thread. Besides, you've added the overhead of a HTTP request which now makes your code execution speed depend on network latency. Not a good idea at all.

Stuart Langridge: Not blocking the UI in tight JavaScript loops | Full-Linux.com

[...] search Stuart Langridge: Not blocking the UI in tight JavaScript loops is now available in this link…: News [...]

Nick Fitzsimons

Why not use

var timer = setInterval(doWork, 0);

with clearInterval(timer) when the termination condition is satisfied?

sil

Dao: yep, I know about 1 not really meaning 1ms. I think that the order is not guaranteed, though; asking for 1ms (or 20ms) doesn't guarantee you get it, so the browser might delay one of the things for 200ms just because it feels like it and then it'll happen after its later-fired brethren. (I don't think that you're guaranteed to get setTimeouted things in the order in which you call them if the timings are the same; if you can point me at something which does actually guarantee that, go for it though!)

Nick: 'cos I didn't think of it. Good approach.

Interesting Finds: 2009 07.01 ~ 07.05 - gOODiDEA.NET

[...] Not blocking the UI in tight JavaScript loops [...]

James Henstridge

Stuart: if you are only setting the timeout for item N+1 after item N has been processed, then it doesn't matter what the browser rounds the time out values to.

If you run two loops in parallel it is possible that the items won't be run in a deterministic order, but you can still run multiple loops sequentially by kicking off the second one when the first completes. If you are performing two independent tasks, the ability to intermingle them might actually be desirable.

sil

jamesh: you're quite right. Don't know what I was thinking of. d'oh.

Weekly Web Nuggets #70 : Code Monkey Labs

[...] Not Blocking the UI in Tight JavaScript Loops: Stuart Langridge shows a simple way to avoid locking up the browser. [...]

The Technology Post for July 6th, 2009 | I love .NET!

[...] JavaScript – Not blocking the UI in tight JavaScript loops – Tweeted by Elijah Manor [...]

The Technology Post for July 6th, 2009 | Nexo IT - Information Technology News

[...] JavaScript – Not blocking the UI in tight JavaScript loops – Tweeted by Elijah Manor [...]

Todd Cullen

This is a good technique to chunk work out over multiple frames. Nice writeup. FYI - the term generally used to describe this is "Green Threading".

Cheers.

Kieran

This is definitely a very useful technique, I'm finding it completely essential for more complex JavaScript apps which require templating and parsing of data by JavaScript itself, where typically the UI would be completely locked up for the duration, obviously providing a terrible user experience.

One annoyance I have with doing things this way is that I can't quite figure out how to know when the loop has finished so you can continue normal execution, as what happens is that code immediately following this loop gets executed before the loop has run, essentially my question is how you and others have dealt with or solved this problem.

Cheers,

Kieran

sil

Kieran: kick off the next action in the UI updater when loopcount = the number of things to be processed.

This website belongs to Stuart Langridge. Contact details are available. Don't eat yellow snow. Valid HTML5, at least in theory, except for the bits that aren't because I'm that futuristic that I'm ahead of the spec, oh yes. HTML5 help from Bruce Lawson, among others. Fonts from the superb FontSquirrel. End.