29

I have such a function in my JS script:

function heavyWork(){
   for (i=0; i<300; i++){
        doSomethingHeavy(i);
   }
}

Maybe "doSomethingHeavy" is ok by itself, but repeating it 300 times causes the browser window to be stuck for a non-negligible time. In Chrome it's not that big of a problem because only one Tab is effected; but for Firefox its a complete disaster.

Is there any way to tell the browser/JS to "take it easy" and not block everything between calls to doSomethingHeavy?

Gadi A
  • 3,449
  • 8
  • 36
  • 54
  • You could try sleeping between calls to `doSomethingHeavy`. Also, if this function deals with communication with some server, AJAX might be the right way to go. – Eran Zimmerman Gonen Apr 16 '12 at 19:30
  • @EranZimmerman: I don't think you can "sleep" in JavaScript. Best you can do is `setTimeout` each call to `doSomethingHeavy`. – gen_Eric Apr 16 '12 at 19:34

9 Answers9

26

You could nest your calls inside a setTimeout call:

for(...) {
    setTimeout(function(i) {
        return function() { doSomethingHeavy(i); }
    }(i), 0);
}

This queues up calls to doSomethingHeavy for immediate execution, but other JavaScript operations can be wedged in between them.

A better solution is to actually have the browser spawn a new non-blocking process via Web Workers, but that's HTML5-specific.

EDIT:

Using setTimeout(fn, 0) actually takes much longer than zero milliseconds -- Firefox, for example, enforces a minimum 4-millisecond wait time. A better approach might be to use setZeroTimeout, which prefers postMessage for instantaneous, interrupt-able function invocation, but use setTimeout as a fallback for older browsers.

SirPeople
  • 4,248
  • 26
  • 46
apsillers
  • 112,806
  • 17
  • 235
  • 239
  • 1
    That should be `setTimeout((function(i){` – gen_Eric Apr 16 '12 at 19:36
  • Both the techniques are useful (for different kinds of heavy work) in my case. Thanks! – Gadi A Apr 17 '12 at 07:48
  • 3
    Nice work. I think it might be worth noting that the browser will be frozen during the duration of `doSomethingHeavy`, so if there are long-operations, there will still be a hang, but it'll be 1/300 of the time. – vol7ron Apr 17 '12 at 12:35
  • Why might setZeroTimeout be a better approach? It's a bit unfair right? The purpose of setTimeout here has not so much to do with the delay but to give other code the chance to run. – Tobbe Brolin Jan 08 '19 at 16:54
  • @TobbeBrolin The problem with `setTimeout(fn, 0)` here is (was? this is 6 years old now) that `setTimeout` has a *minimum* wait time of 4 milliseconds. If you need to run `fn` 1000 times, that's an upper bound of 4 extra seconds of wasted time. By waiting "zero" milliseconds, you still make room for other processes to butt in between iterations (you are indeed still deferring to any queued waiting tasks with `setZeroTimeout`), but you don't wasted 4 ms if there is nothing else waiting the task queue. – apsillers Jan 08 '19 at 19:34
12

You can try wrapping each function call in a setTimeout, with a timeout of 0. This will push the calls to the bottom of the stack, and should let the browser rest between each one.

function heavyWork(){
   for (i=0; i<300; i++){
        setTimeout(function(){
            doSomethingHeavy(i);
        }, 0);
   }
}

EDIT: I just realized this won't work. The i value will be the same for each loop iteration, you need to make a closure.

function heavyWork(){
   for (i=0; i<300; i++){
        setTimeout((function(x){
            return function(){
                doSomethingHeavy(x);
            };
        })(i), 0);
   }
}
gen_Eric
  • 223,194
  • 41
  • 299
  • 337
  • 1
    `0` is not an acceptable value for a delay anymore, you'll need to use at least `4`. – Teemu Apr 16 '12 at 19:34
  • 1
    @Teemu: [citation needed] EDIT: found it https://developer.mozilla.org/en/DOM/window.setTimeout#Minimum_delay_and_timeout_nesting – gen_Eric Apr 16 '12 at 19:37
  • 3
    @Teemu: According to the [spec](http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#timers), this should happen automatically: `If the currently running task is a task that was created by the setTimeout() method, and timeout is less than 4, then increase timeout to 4`. – gen_Eric Apr 16 '12 at 19:45
  • I just found the same MDN-link for you... It is already here. Lately I've been "translating" some old HTAs to use IE9 utilities, and I've had to change every single timeout- and interval-delay smaller than 4, to 4. If below, no calls are invoked. – Teemu Apr 16 '12 at 19:52
  • @Teemu: Weird, I've had no issues with using a `0` delay for `setTimeout` in IE9. – gen_Eric Apr 16 '12 at 19:54
  • Potential problem: there is no guarantee that `setTimeout` executes its callbacks in the order that they are scheduled. – dyoo Apr 16 '12 at 19:55
  • @dyoo: I've never had that issue. – gen_Eric Apr 16 '12 at 19:57
  • 1
    @rocket: see http://stackoverflow.com/questions/1776239/are-equal-timeouts-executed-in-order-in-javascript – dyoo Apr 16 '12 at 20:01
  • @Rocket I wish I hadn't either, there was a lot of stuff to fix. But as far as I can understand english, this (`then increase timeout to 4`) means you have to do it your self. – Teemu Apr 16 '12 at 20:02
  • @Teemu: That's the logic the JavaScript engine goes through when running `setTimeout`. It'll increase it to 4 internally, you shouldn't have to. – gen_Eric Apr 16 '12 at 20:03
  • @Rocket I see, this (whatwg.org) seems to be a very usefull link, thanks. – Teemu Apr 16 '12 at 20:13
  • @Rocket: +1, just noticed my edit was almost the same as your answer ;) – vol7ron Apr 17 '12 at 02:54
10

You need to use Web Workers

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

There are a lot of links on web workers if you search around on google

vsync
  • 118,978
  • 58
  • 307
  • 400
ControlAltDel
  • 33,923
  • 10
  • 53
  • 80
3

We need to release control to the browser every so often to avoid monopolizing the browser's attention.

One way to release control is to use a setTimeout, which schedules a "callback" to be called at some period of time. For example:

var f1 = function() {
    document.body.appendChild(document.createTextNode("Hello"));
    setTimeout(f2, 1000);
};

var f2 = function() {
    document.body.appendChild(document.createTextNode("World"));
};

Calling f1 here will add the word hello to your document, schedule a pending computation, and then release control to the browser. Eventually, f2 will be called.

Note that it's not enough to sprinkle setTimeout indiscriminately throughout your program as if it were magic pixie dust: you really need to encapsulate the rest of the computation in the callback. Typically, the setTimeout will be the last thing in a function, with the rest of the computation stuffed into the callback.

For your particular case, the code needs to be transformed carefully to something like this:

var heavyWork = function(i, onSuccess) {
   if (i < 300) {
       var restOfComputation = function() {
           return heavyWork(i+1, onSuccess);
       }
       return doSomethingHeavy(i, restOfComputation);          
   } else {
       onSuccess();
   }
};

var restOfComputation = function(i, callback) {
   // ... do some work, followed by:
   setTimeout(callback, 0);
};

which will release control to the browser on every restOfComputation.

As another concrete example of this, see: How can I queue a series of sound HTML5 <audio> sound clips to play in sequence?

Advanced JavaScript programmers need to know how to do this program transformation or else they hit the problems that you're encountering. You'll find that if you use this technique, you'll have to write your programs in a peculiar style, where each function that can release control takes in a callback function. The technical term for this style is "continuation passing style" or "asynchronous style".

Community
  • 1
  • 1
dyoo
  • 11,795
  • 1
  • 34
  • 44
3

You can make many things:

  1. optimize the loops - if the heavy works has something to do with DOM access see this answer
  • if the function is working with some kind of raw data use typed arrays MSDN MDN
  1. the method with setTimeout() is called eteration. Very usefull.

  2. the function seems to be very straight forward typicall for non-functional programming languages. JavaScript gains advantage of callbacks SO question.

  3. one new feature is web workers MDN MSDN wikipedia.

  4. the last thing ( maybe ) is to combine all the methods - with the traditional way the function is using only one thread. If you can use the web workers, you can divide the work between several. This should minimize the time needed to finish the task.

vsync
  • 118,978
  • 58
  • 307
  • 400
Bakudan
  • 19,134
  • 9
  • 53
  • 73
2

I see two ways:

a) You are allowed to use Html5 feature. Then you may consider to use a worker thread.

b) You split this task and queue a message which just do one call at once and iterating as long there is something to do.

stefan bachert
  • 9,413
  • 4
  • 33
  • 40
1
function doSomethingHeavy(param){
   if (param && param%100==0) 
     alert(param);
}

(function heavyWork(){
    for (var i=0; i<=300; i++){
       window.setTimeout(
           (function(i){ return function(){doSomethingHeavy(i)}; })(i)
       ,0);
    }
}())
vol7ron
  • 40,809
  • 21
  • 119
  • 172
  • The code above is ineffective. setTimeout does not magically save the surrounding computational context, nor does it yield control back to the browser. It's a scheduling mechanism. The explanation above is wrong. – dyoo Apr 16 '12 at 19:46
  • @dyoo: I don't think that's right, maybe the proper way would be to `0`, but I'm pretty sure it jumps to the next in the stack, while it's waiting. – vol7ron Apr 16 '12 at 20:07
  • No. As proof, try: `var test = function() { var f = function() {alert('hi!');}; setTimeout(f, 0); while(true) {} }; test()`. In your presentation, this would say 'hi!'. Note that it does not do so. I'm trying to point out that just adding `setTimeout()` is insufficient. Calling `setTimeout` does not yield control back to the browser: it's only a scheduling mechanism. – dyoo Apr 16 '12 at 20:11
  • I see what you're saying and you're right, it is about scheduling (but so is browser control). The problem is that it has to occur outside of `doSoemthingHeavy`, unless you create your own queue stack and check that from w/in `doSomethingHeavy`. It seems there are a lot of answers since I initially posted this, so I'll just delete after I've given you some time to see this comment. – vol7ron Apr 16 '12 at 21:41
1

There was a person that wrote a specific backgroundtask javascript library to do such heavy work.. you might check it out at this question here:

Execute Background Task In Javascript

Haven't used that for myself, just used the also mentioned thread usage.

Community
  • 1
  • 1
Stefan
  • 2,603
  • 2
  • 33
  • 62
0

There is a feature called requestIdleCallback (pretty recently adopted by most larger platforms) where you can run a function that will only execute when no other function takes up the event loop, which means for less important heavy work you can execute it safely without ever impacting the main thread (given that the task takes less than 16ms, which is one frame. Otherwise work has to be batched)

I wrote a function to execute a list of actions without impacting main thread. You can also pass a shouldCancel callback to cancel the workflow at any time. It will fallback to setTimeout:

export const idleWork = async (
  actions: (() => void)[],
  shouldCancel: () => boolean
): Promise<boolean> => {
  const actionsCopied = [...actions];
  const isRequestIdleCallbackAvailable = "requestIdleCallback" in window;

  const promise = new Promise<boolean>((resolve) => {
    if (isRequestIdleCallbackAvailable) {
      const doWork: IdleRequestCallback = (deadline) => {
        while (deadline.timeRemaining() > 0 && actionsCopied.length > 0) {
          actionsCopied.shift()?.();
        }

        if (shouldCancel()) {
          resolve(false);
        }

        if (actionsCopied.length > 0) {
          window.requestIdleCallback(doWork, { timeout: 150 });
        } else {
          resolve(true);
        }
      };
      window.requestIdleCallback(doWork, { timeout: 200 });
    } else {
      const doWork = () => {
        actionsCopied.shift()?.();
        if (shouldCancel()) {
          resolve(false);
        }

        if (actionsCopied.length !== 0) {
          setTimeout(doWork);
        } else {
          resolve(true);
        }
      };
      setTimeout(doWork);
    }
  });

  const isSuccessful = await promise;
  return isSuccessful;
};

The above will execute a list of functions. The list can be extremely long and expensive, but as long as every individual task is under 16ms it will not impact main thread. Warning because not all browsers supports this yet, but webkit does

Gabriel Petersson
  • 8,434
  • 4
  • 32
  • 41
  • The `requestIdleCallback` is not new at all, and exists for many years. Maybe it's new to you ;) You should edit your question and remove the *"2022 Answer"* title, because it's obvious you wrote it in 2022 (seen below your answer, above your username). Writing the date of an answer in huge font, is not something of importance and is also misleading in this current answer – vsync Aug 09 '22 at 14:34
  • @vsync new in terms of platform adoption of course, which is what matters right? so yes, new relative other features that have adoption for multiple years back – Gabriel Petersson Aug 21 '22 at 18:40
  • Sorry but It is not new however you spins this. Also, your last edit should probably be re-edited (try reading your answer) – vsync Aug 21 '22 at 20:50