9

When looking to improve a page's performance, one technique I haven't heard mentioned before is using setTimeout to prevent javascript from holding up the rendering of a page.

For example, imagine we have a particularly time-consuming piece of jQuery inline with the html:

$('input').click(function () {
    // Do stuff
});

If this code is inline, we are holding up the perceived completion of the page while the piece of jquery is busy attaching a click handler to every input on the page.

Would it be wise to spawn a new thread instead:

setTimeout(function() {
    $('input').click(function () {
        // Do stuff
    })
}, 100);

The only downside I can see is that there is now a greater chance the user clicks on an element before the click handler is attached. However, this risk may be acceptable and we have a degree of this risk anyway, even without setTimeout.

Am I right, or am I wrong?

cbp
  • 25,252
  • 29
  • 125
  • 205
  • I guess it will heavily depend on the browser if it will be faster or not. Have you benchmarked any rendering with it? – stefan Feb 25 '11 at 05:14
  • 3
    `setTimeout` doesn't spawn a new thread. Instead, it queues work for the single thread of execution at some point in the future. – ephemient Feb 25 '11 at 05:19
  • Alternatively, consider placing your time consuming code inside a $(document).ready(function() {...}); – Xenethyl Feb 25 '11 at 05:24
  • 3
    This improves *responsiveness*, not *performance*. – Ben Voigt Feb 25 '11 at 05:29
  • 1
    Be careful about how often you use the `setTimeout` idiom (see @ephemient's comment). The folks at google did a great piece on over[ab]using timers with ironically sluggish consequences: http://googlecode.blogspot.com/2009/07/gmail-for-mobile-html5-series-using.html. – Crescent Fresh Feb 25 '11 at 06:09

3 Answers3

12

The actual technique is to use setTimeout with a time of 0.

This works because JavaScript is single-threaded. A timeout doesn't cause the browser to spawn another thread, nor does it guarantee that the code will execute in the specified time. However, the code will be executed when both:

  1. The specified time has elapsed.
  2. Execution control is handed back to the browser.

Therefore calling setTimeout with a time of 0 can be considered as temporarily yielding to the browser.

This means if you have long running code, you can simulate multi-threading by regularly yielding with a setTimeout. Your code may look something like this:

var batches = [...]; // Some array
var currentBatch = 0;

// Start long-running code, whenever browser is ready
setTimeout(doBatch, 0);

function doBatch() {
    if (currentBatch < batches.length) {
        // Do stuff with batches[currentBatch]

        currentBatch++;
        setTimeout(doBatch, 0);
    }
}

Note: While it's useful to know this technique in some scenarios, I highly doubt you will need it in the situation you describe (assigning event handlers on DOM ready). If performance is indeed an issue, I would suggest looking into ways of improving the real performance by tweaking the selector.

For example if you only have one form on the page which contains <input>s, then give the <form> an ID, and use $('#someId input').

Community
  • 1
  • 1
David Tang
  • 92,262
  • 30
  • 167
  • 149
  • 3
    While I haven't tested this with the most modern bunch of browsers, I've run into issues in the past with using 0 as the timeout: some browser was short-circuiting it to a synchronous execution. I therefore always use 1. – Ates Goral Feb 25 '11 at 05:42
  • @Ates, I've not run into this before but that's very interesting, thanks for the info. – David Tang Feb 25 '11 at 05:45
  • 2
    @Ates: What you describe should *never* happen. What browser? What context? – Crescent Fresh Feb 25 '11 at 06:08
  • @Crescent Fresh: How can you be so sure? Unless of course you know by heart the JS engine internals of all browsers to date... I just happen to remember seeing this at least once. I don't remember exactly what browser or what context or what year, but I'm pretty sure I saw it. – Ates Goral Feb 25 '11 at 06:34
  • @Crescent Fresh: Also, just to clarify, by "treating it as synchronous", I didn't mean that it inlined the callback: it simply executed the function at the tail end of the current JS thread, without yielding to the browser (or DOM). I wish I had the means to test this again with some older browsers, but I don't. We'll have to live with my anecdote unless you have _proof_ that this is impossible. Having said that, I admit that there's still a chance that my interpretation of the symptom was wrong. – Ates Goral Feb 25 '11 at 06:42
  • Yikes! http://code.google.com/p/chromium/issues/detail?id=888 "setTimeout(..., 0) fires too quickly" bug reported by the Man himself. I guess I wasn't dreaming... ;) – Ates Goral Feb 25 '11 at 06:45
  • 1
    @Ates, the bug report (and ensuing comments) does not mention synchronicity, which is the main thing. The fact that the chromium timer does not have a minimum (though in fact it still has 1ms) doesn't mean the code doesn't yield to the browser. – David Tang Feb 25 '11 at 06:55
  • @Box9: Yes, that bug report looks unrelated. It is likely that I had made that observation due to modifications to DOM not being ready when I was expecting them to be. I wouldn't have been ticked off by a short timer (i.e. 1 ms versus 10+ ms like it's mentioned in that bug report), but with some runtime error due to not being able to access a new DOM element that I expect to be ready. Anyways... My claim cannot go beyond an anecdote/speculation level since I don't have any means of proving it anymore. So, I'll just accept defeat and let it go for now :) – Ates Goral Feb 25 '11 at 07:11
3

setTimeout() can be used to improve the "perceived" load time -- but not the way you've shown it. Using setTimeout() does not cause your code to run in a separate thread. Instead setTimeout() simply yields the thread back to the browser for (approximately) the specified amount of time. When it's time for your function to run, the browser will yield the thread back to the javascript engine. In javascript there is never more than one thread (unless you're using something like "Web Workers").

So, if you want to use setTimeout() to improve performance during a computation-intensive task, you must break that task into smaller chunks, and execute them in-order, chaining them together using setTimeout(). Something like this works well:

function runTasks( tasks, idx ) {
   idx = idx || 0;
   tasks[idx++]();
   if( idx < tasks.length ) {
      setTimeout( function(){ runTasks(tasks, idx); },1);
   }
}
runTasks([
   function() {
      /* do first part */
   },
   function() {
      /* do next part */
   },
   function() {
      /* do final part */
   }
]);

Note:

  1. The functions are executed in order. There can be as many as you need.
  2. When the first function returns, the next one is called via setTimeout().
  3. The timeout value I've used is 1. This is sufficient to cause a yield, and the browser will take the thread if it needs it, or allow the next task to proceed if there's time. You can experiment with other values if you feel the need, but usually 1 is what you want for these purposes.
Lee
  • 13,462
  • 1
  • 32
  • 45
  • Instead of wrestling with an index, you could `shift` elements out of the array after testing its `length`. – Ates Goral Feb 25 '11 at 05:59
  • yes, that would work too. It depends on whether it's ok to have the tasks array destroyed in the process. Also, the simple manipulation of an index should perform substantially better than the shift operation (though admittedly performance is probably not the primary concern in this particular case). – Lee Feb 25 '11 at 06:12
-1

You are correct, there is a greater chance of a "missed" click, but with a low timeout value, its pretty unlikely.

Chris Cherry
  • 28,118
  • 6
  • 68
  • 71