Redu
Redu

Reputation: 26161

Updating DOM before blocking code by setTimeout or promise

I know that when there is a CPU intensive code any immediate previous DOM update won't happen. Such as

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}

result.textContent = "Please remain..."; // we will never see this
blockFor(2000);
<p id="result"></p>

However if I shift the CPU intensive code to the asynchronous timeline by setTimeout it's all fine as in the following snippet.

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}

result.textContent = "Please remain..."; // now you see me
setTimeout(_ => blockFor(2000),15);      // 15ms to be on the safe side
<p id="result"></p>

However since i know that promises also take you to a "sort of" asycnronous timeline i was expecting to achieve the same effect without using the setTimeout hack. Such as;

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}

result.textContent = "Please remain..."; // not in Chrome not in FF
Promise.resolve(2000)
       .then(blockFor)
<p id="result"></p>

I would at least expect this to run as expected in FF because of this perfect article (https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) alas no way.

Is there any way to accomplish this job with promises?

Upvotes: 5

Views: 1659

Answers (3)

gadi tzkhori
gadi tzkhori

Reputation: 612

Best way to do it is to delegate the heavy process to a web worker...

// main thread

document.getElementById("result").addEventListener('click', handleClick);
const worker = new Worker('worker.js');


function handleClick(){
  worker.onmessage = e => {
   console.log('main', e.data.response)  
   this.textContent = e.data.response;
  }
  this.textContent = "Please remain...";
  worker.postMessage({data: 2000});
}

// worker

self.addEventListener('message', e => {
    const { data } = e.data;
    console.log('worker', data); 

    function blockFor(dur){
     var now = new Date().getTime();
     while (new Date().getTime() < now + dur);
     }

    blockFor(data)
    self.postMessage({ response: "I am done..!" });
});



  // NOTE: perform this test on your app for browser compatibility
  if (window.Worker) {
  ...

}

Check out this live code

MDN web workers docs

Upvotes: 0

Oriol
Oriol

Reputation: 288120

The problem is that the promise, even if it runs asynchronously, runs too early. So browsers don't have time to update the DOM. This problem is not specific to promises, I see the same result when using a setTimeout with a 0ms delay:

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}
result.textContent = "Please remain..."; // we will never see this
setTimeout(_ => blockFor(2000), 0);      // 0ms is not enough
<p id="result"></p>

In fact, it seems what you want is requestAnimationFrame:

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}
result.textContent = "Please remain..."; // now you see me
new Promise(function(resolve) {
  requestAnimationFrame(_ => resolve(2000));
}).then(blockFor);
<p id="result"></p>

But at this point you could use requestAnimationFrame alone, without promises.

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}
result.textContent = "Please remain..."; // now you see me
requestAnimationFrame(_ => blockFor(2000));
<p id="result"></p>

Upvotes: 1

Benjamin Gruenbaum
Benjamin Gruenbaum

Reputation: 276306

Promise.prototype.then has microtask semantics. This means it has to wait for synchronous code to run but not for asynchronous code to run - browsers probably choose to wait for all JS to run before doing DOM updates.

Generally microtask means it has to wait for other JS to run, and then it can run before yielding control to non JS code.

setTimeout has macrotask semantics. It runs as a part of the DOM API and when the callback runs the non-js code has already gotten a chance to run. Browsers already run their own code when this runs so they also process events and DOM updates.

Generally macrotask means that it has to wait for all other JS to run and also for the "event loop to tick" - that is: events to fire.

This is also the difference between setImmediate and nextTick in NodeJS.

To answer your question directly: no. There is no way to force the browser to run DOM updates in a microtick update - while it is technically not forbidden for it to do so - it would be "bad mannered".

For long running CPU bound operations - may I suggest Web Workers instead?

Upvotes: 3

Related Questions