Gadi A
Gadi A

Reputation: 3539

Javascript - how to avoid blocking the browser while doing heavy work?

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?

Upvotes: 30

Views: 19602

Answers (9)

Gabriel Petersson
Gabriel Petersson

Reputation: 10492

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

Upvotes: 1

Bakudan
Bakudan

Reputation: 19492

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.

Upvotes: 3

ControlAltDel
ControlAltDel

Reputation: 35106

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

Upvotes: 10

apsillers
apsillers

Reputation: 116040

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.

Upvotes: 27

vol7ron
vol7ron

Reputation: 42179

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);
    }
}())

Upvotes: 1

dyoo
dyoo

Reputation: 12033

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".

Upvotes: 3

gen_Eric
gen_Eric

Reputation: 227310

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);
   }
}

Upvotes: 12

stefan bachert
stefan bachert

Reputation: 9624

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.

Upvotes: 2

Stefan
Stefan

Reputation: 2613

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.

Upvotes: 1

Related Questions