1

I have a long running task in JavaScript, which I break up in chunks with a series of nested setTimeout(processChunk, 0), similar to what is described here. However, for each invocation, setTimeout adds an additional delay of 4 ms or more. This behaviour is well known and varies across browsers.

When I attempt to keep the processing time of each chunk at 50 ms or less, these additional delays increase total processing time by at least 10%.

My question is: Can I avoid the additional delay (and thus improve the processing speed) while maintaining backwards compatibility with ES3 browsers and old IE browsers?

Tomas Langkaas
  • 4,551
  • 2
  • 19
  • 34

1 Answers1

2

There is a straightforward workaround for this issue. Since the minimum delay of setTimeout is measured from when the timer is set, make sure to set timers at least 10–15 ms before each chunk should be processed. When several setTimeout are set, they queue up and the next one is invoked immediately after the previous, without the additional delay. This can be done with only 2 active timers:

function runLongTask() {
  var complete = false;
  function processChunk() {
    if(!complete) {
      /* ... process chunk, set complete flag after last chunk ... */
      //set new timer
      setTimeout(processChunk);
    } else {
      /* ... code to run on completion ... */
    }
  }
  //set a timer to start processing
  setTimeout(processChunk);
  //set an extra timer to make sure 
  //there are always 2 active timers,
  //this removes the extra delay provided
  //that processing each chunk takes longer
  //than the forced delay
  setTimeout(processChunk);
}

Below is a working demo comparing the workaround approach to the traditional approach of setting a new setTimeout after each chunk is processed. In the workaround there is always an extra setTimeout set ahead, reducing the processing time with about 4 ms or more for each chunk (about 40 ms or more for 10 chunks, as demonstrated below), provided that each chunk takes at least 4 ms to process. Note that the workaround demonstrates the use of only 2 active timers.

function runForAtLeast15ms() {
  var d = (+new Date) + 15;
  while(+new Date < d);
}

function testTimeout(repetitions, next, workaround) {
  var startTime = +new Date;

  function runner() {
    if(repetitions > 0) {
      //process chunk
      runForAtLeast15ms();
      //set new timer
      setTimeout(runner);
    } else if(repetitions === 0) {
      //report result to console
      console.log((workaround? 'Workaround' : 'Traditional') + 
                  ' approach: ' +
                  ((+new Date) - startTime) + ' ms');
      //invoke next() function if provided
      next && next();
    }
    repetitions--;
  }

  setTimeout(runner);

  if(workaround){
   //make sure that there are always 2
   //timers running by setting an extra timer
   //at start
   setTimeout(runner);
  }
}

//First: repeat runForAtLeast15ms 10 times
//with repeated setTimeout
testTimeout(10, function(){
  //Then: repeat runForAtLeast15ms 10 times
  //using a repeated set of 2 setTimeout
  testTimeout(10, false, true);
});
Tomas Langkaas
  • 4,551
  • 2
  • 19
  • 34
  • You answered your own question at the same time you asked the question. – Stubbies Nov 22 '16 at 22:35
  • @Everald, That's right. [It is encouraged by SO](https://stackoverflow.blog/2011/07/its-ok-to-ask-and-answer-your-own-questions/). I struggled with this question on my own recently and found a solution I would like to document and share. By sharing, someone may even provide a better solution. – Tomas Langkaas Nov 22 '16 at 22:37