5

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?

Redu
  • 25,060
  • 6
  • 56
  • 76
  • What do you find wrong with the result of executing the last block of code? What is your expected behavior? – jfriend00 Aug 27 '16 at 18:26
  • @jfriend00 promise takes my code to asynchronous timeline and i would expect the DOM to be refreshed meanwhile just like `setTimeout(_ => block(2000),0)` would do. – Redu Aug 27 '16 at 18:31
  • Geez. If your real problem you're trying to solve is how to update the screen during a long operation, then please modify your question to state that that is the actual issue you are trying to solve. Your title kind of says that, but the text of your question asks all sorts of other non-related things that invite explanations to go into lots of things that have nothing to do with your actual root problem. – jfriend00 Aug 27 '16 at 18:33
  • And, do some searches on forcing an update of the DOM. There are tons and tons of articles on that topic. Promises are not guaranteed to do what you want. The promise specification only requires that the stack be unwound and have only platform code on it. Nowhere does it require that a screen repaint can happen before a `.then()` handler fires. In fact, some browser optimizations may explicitly try to delay the screen update until any pending promise handlers have executed. – jfriend00 Aug 27 '16 at 18:35
  • @jfriend00 "If your real problem you're trying to solve is how to update the screen during a long operation" Not during.. before.. "In fact, some browser optimizations may explicitly try to delay the screen update until any pending promise handlers have executed." This i didn't know thank you.. Are there any resources that you can point. I would love to read... – Redu Aug 27 '16 at 18:58
  • So, notice you got an answer to the question you asked, but not a solution to the problem you have because you didn't actually describe your real problem - presumably you'd get better help if next time you explain the overall problem and ask about that along with your attempted solution. Lots of articles on forcing a repaint: https://www.google.com/#q=how+to+force+browser+repaint. I know browsers have changed their behavior form time to time so I'm not personally aware of what the best scheme is right now. It would take some reading and some testing. – jfriend00 Aug 27 '16 at 19:09

3 Answers3

3

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?

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    Thank you for your perfect answer. "This means it has to wait for the synchronous code to run but not for asynchronous code to run"... so the DOM update falls behind the ` .then` execution always. "To answer your question directly: no." Thank you... this was the answer was expecting to read. – Redu Aug 27 '16 at 19:04
  • @Redu I'm glad I could help - by the way, libraries like Bluebird offer a `setScheduler` method that would allow you to always schedule `then` callbacks behind a `setTimeout`. – Benjamin Gruenbaum Aug 27 '16 at 19:44
  • Well... i am a very library agnostic person. I am pretty sure it all boils down to adding a`setTimeout(callback,17)` at a `.then` stage just like it would serve the same purpose with Oriol's (below) very wise, nested `requestAnimationFrame()` solution. But again thank you so much for your best answer. – Redu Aug 27 '16 at 20:07
  • Oriol's answer breaks on mobile browsers - and RAF has a lot of scheduling issues with inactive tabs. If you can live with that by all means nest RAFs but note that in inactive tabs a RAF might not fire at all. – Benjamin Gruenbaum Aug 27 '16 at 20:16
1

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>
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • 1
    Only your first snippet works for me in Chrome 52.0.2743.116 m on Windows 10. In the last two, the window stays blank until it says I am done! and it never shows the Please remain... This stuff is non-standard and varies by browser. Solutions have to be regularly tested in all common browsers. – jfriend00 Aug 27 '16 at 19:12
  • 1
    @Oriol Thank you for your answer. `requestAnimatioFrame()` is something i have tried instead of `setTimeout()`. No I doesn't work in Chrome but in FF it does. I believe there is a huge lack of standardization right at this point. + for pointing out though. – Redu Aug 27 '16 at 19:12
  • @Redu Uh, it's strange it doesn't work on Chrome. I think `requestAnimatioFrame` is supposed to run at 60Hz. That's 16.7ms, which is more than the 15ms delay in the timeout. – Oriol Aug 27 '16 at 19:21
  • @Oriol Yes but it's all about exactly at what time you fire the `requestAnimationFrame()`. There is no guarantee you fire it right after the previous one. May be you fire it just 1ms before the next one to happen. At least that's what i have thought when it failed in Chrome. Then i thought engines must be handling it differently since it is OK with FF. – Redu Aug 27 '16 at 19:25
  • 1
    @Redu True. I read at [the spec](//html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model:run-the-animation-frame-callbacks) that animation frames run before updating the rendering, so what Chrome does makes sense. I guess a `requestAnimationFrame` inside a `requestAnimationFrame` would work, unless the browser "believes would not benefit from having their rendering updated at this time", "a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates". It seems a browser could wait a year before repainting, and still follow the spec. – Oriol Aug 27 '16 at 19:39
  • @Oriol a nested `requestAnimationFrame()` made me smile. Just because I find it wise. I am almost sure it would work in Chrome... But again it will all boil down to `setTimeout(_ => blockFor(2000),17)` wouldn't it..? Well i guess there is no way do accomplish the task with the promises as for the main point of my question. – Redu Aug 27 '16 at 19:48
0

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

gadi tzkhori
  • 574
  • 2
  • 13