3

In code snippet below the promise is recursively called after completion of itself. I ran the following code snippet on Edge and on Chrome and am seeing a marked difference in execution time. Is there something wrong that I am doing? How I can improve the execution time on Edge?

Please ignore the promise 'anti-pattern'. It is just to simulate an asynchronous method.

Chrome result - Count - 99999 Start 1512416096194 last update 1512416096509 End 1512416096577 recursiveFnReturnsPromise finished

execution time - 397ms

Edge result - Count - 99999 Start 1512415183349 last update 1512415508413 End 1512415907219 recursiveFnReturnsPromise finished

execution time between the start and the end- 723870ms

execution time between last the DOM update and the end- 398806ms

//large array
let aValues = Array(100000);

//utility function
function writeln(text) {
  let p = document.createElement('div');
  p.innerText = text;
  document.getElementById('v1').appendChild(p);
}


writeln('Start ' +Date.now()); //note start recursion time

recursiveFnReturnsPromiseV1(aValues, 1).then(function() {
  writeln('End ' +Date.now());//note end recursion time
  writeln('recursiveFnReturnsPromise finished');
}, function() {
  writeln('End' +Date.now());
  writeln('recursiveFnReturnsPromise failed');
})

//the recursive functions which returns a promise
function recursiveFnReturnsPromiseV1(pValues, ix) {
  if (pValues.length <= ix)
    return Promise.resolve();

  return new Promise(function(c, e) {
    document.getElementById('output').innerText = ix;
    if(ix==99999) writeln('last update ' +Date.now());
    c();
  }).then(function() {
    return recursiveFnReturnsPromiseV1(pValues, ++ix);
  })
}
Count - <span id='output'></span>
<div id='v1'></div>

Note - for anyone interested in trying to do promise recursion right please see related post - What is the difference in following pattern to call recursive JavaScript function which returns a promise?

smile.al.d.way
  • 361
  • 5
  • 17
  • 1
    Does it make a difference if you take out that line that sets the "output" element to the index? My guess would be that Edge may be doing layout work that Chrome doesn't. – Pointy Dec 04 '17 at 17:32
  • Interesting, Firefox shows a majority of time spent in "JIT" (which, uh, doesn't make sense to me). What do you see in Edge? https://learn.microsoft.com/en-us/microsoft-edge/f12-devtools-guide/performance – Josh Lee Dec 04 '17 at 18:08
  • Running this snippit myself, Firefox Nightly (59.0a1) => `3 611`; Edge (41.16299.15.0) => `1 607`; Chrome Canary (64.0.3282.5) => `179`. Yes, dramatically longer, but I don't see 17 seconds anywhere. – msanford Dec 04 '17 at 18:57
  • 12 *minutes*? Really? – Bergi Dec 04 '17 at 19:30
  • Actually there is no promise constructor antipattern in this code, it's not creating another promise inside the `new Promise` callback. Admittedly, the synchronous fulfillment could have been simplified to `Promise.resolve()`. – Bergi Dec 04 '17 at 19:33
  • "*Is there something wrong that I am doing?*" - Yes: you try to "*simulate an asynchronous method*" without doing anything actually asynchronous. It's true that your results in Edge are horrific, but this is a really unusual case. Normally most of the execution time will be spent at the asynchronous task(s), not at the promises - and for looping only, a synchronous loop should be used. – Bergi Dec 04 '17 at 19:38
  • If there was a real asynchronous task running within won't it only add to total execution time which we see here for Edge? Am I not understanding this correct? – smile.al.d.way Dec 04 '17 at 19:42
  • 1
    A better way to simulate an asynchronous task is `await new Promise(setTimeout)`. This will allow Chrome and Firefox to draw the counter, for instance, and will match the behavior seen when performing async I/O or message passing. – Josh Lee Dec 04 '17 at 19:59
  • FYI in case anyone wasn't aware, the promises implementation was horribly slow/broken in Edge back in 2017, this was fixed in Edge 14. The above issues can be attributed to that. – Charleh Dec 28 '19 at 12:15

2 Answers2

2

Even without interacting with the DOM, Promises are dramatically slower in Firefox.

(async()=>{
  let start = Date.now();
  for (let i = 0; i < 1e6; i++) await Promise.resolve();
  let end = Date.now();
  console.log(end-start, 'ms');
})();

On my machine, this completes in 0.25 seconds in Node.js, 6 seconds in Chrome, and 16 seconds in Firefox. (I don't have Edge).

I would avoid iterating over an excessive number of Promises. It should be doable — in any realistic code the majority of wall clock time will be spent rendering updates in the browser or waiting for asynchronous calls to complete, and only a small fraction will be in actually executing the asynchronous calls themselves. Perform more work synchronously if that's the case.

This is slightly faster for some reason:

(async()=>{
  let start = Date.now();
  await Promise.all(Array(1e6).fill(Promise.resolve()));
  let end = Date.now();
  console.log(end-start, 'ms');
})();

1 second in Node.js, 2.5 seconds in Chrome, 4 seconds in Firefox. (And in fact, the bulk of the work is in actually building the array, I get <1 second if I move that outside the timed section.)

Using Promise.all might be faster because the script produces the work all at once, but waiting for it to complete does not enter the interpreter (to a very rough approximation). That might be a good workaround if Promises are not as fast as they are in V8.

Josh Lee
  • 171,072
  • 38
  • 269
  • 275
  • I don't think I can use Promise.all. I need them to execute one after another but callee API within are all promise (that is they return promises). And yes, you are correct DOM manipulation has no impact. I tried on Edge browser. – smile.al.d.way Dec 04 '17 at 19:04
  • Regarding performance monitor on edge, I don't think I am able to see what you are looking for. It is not as if the browser is stuck / unresponsive while executing this. The browser keeps updating the "output" element just fine but takes 724387ms to get from 0 to 99999 – smile.al.d.way Dec 04 '17 at 19:06
  • Oh that's interesting! In Chrome and Firefox the update is not seen until the script finishes. By adding a `setTimeout` call to continue the loop it also took a very long time in Chrome and Firefox, since it is of course bound by actually rendering the document. – Josh Lee Dec 04 '17 at 19:07
  • 1
    I have no idea why Edge is rendering the updated `textContent` when the script is clearly not yielding control to the browser. – Josh Lee Dec 04 '17 at 19:08
  • See my updated output results. The Edge after running the counter to 99999 waits for another 398806ms before it finally comes to the end. As if it is clearing its built up stack - which there shouldn't be any -correct? – smile.al.d.way Dec 04 '17 at 19:38
  • @JoshLee I disagree, older Chrome would paint if I interact with dom in a promise resolve function. The stack is empty even though the que is not but since there is no way of knowing when the next in que gets pushed on the stack (setTimeout or when a promise resolves) Chrome would paint on empty stack. New Chrome (and Firefox) will schedule the paint and if something gets pushed on stack from que within a certain amount of time it will not paint but consolidate changes made in the newer functions that came from que (see my anwer). – HMR Dec 05 '17 at 06:40
  • I might recommend: `await new Promise (r => setTimeout (r, ms))` where `ms` is the delay in milliseconds – I would guess `Promise.all` is faster because it's only using a single `await` – Mulan Dec 05 '17 at 20:12
0

It may have something to do with Chrome optimizing paints so when I add an element to dom and then change style in 2 different resolve functions it is handled as one action (one paint) depending how quickly the 2 dom interaction requests were made.

The following won't animate in Chrome:

const el = document.querySelector("#content");
Promise.resolve(el)
.then(
  _=>
    el.innerHTML = `
    <div style="position:absolute;top:100px;transition:top 2s;">hello world</div>
    `
)
.then(
  _ =>
    // setTimeout(x=>el.querySelector("div").style.top = "2px",100)
    el.querySelector("div").style.top = "2px"
);
<div id="content">
</div>

If that one animates in Edge that means your appendChild and innerText causes thousands of paints in edge but doesn't in Chrome, the following will animate in Chrome and Firefox because getBoundingClientRect will flush scheduled changes to the dom, you could use this if add and/or animate elements in a promise chain:

const el = document.querySelector("#content");
Promise.resolve(el)
.then(
  _=>{
    el.innerHTML = `
    <div style="position:absolute;top:100px;transition:top 2s;">hello world</div>
    `;
    //this will trigger paint as well (in case you do want to animate)
    //  it will flush changes to the dom before getting bounding
    el.getBoundingClientRect();
  }
)
.then(
  _ =>
    // setTimeout(x=>el.querySelector("div").style.top = "2px",100)
    el.querySelector("div").style.top = "2px"
);
<div id="content">
</div>
HMR
  • 37,593
  • 24
  • 91
  • 160