14

To see the problem in action, see this jsbin. Clicking on the button triggers the buttonHandler(), which looks like this:

function buttonHandler() {
  var elm = document.getElementById("progress");
  elm.innerHTML = "thinking";
  longPrimeCalc();
}

You would expect that this code changes the text of the div to "thinking", and then runs longPrimeCalc(), an arithmetic function that takes a few seconds to complete. However, this is not what happens. Instead, "longPrimeCalc" completes first, and then the text is updated to "thinking" after it's done running, as if the order of the two lines of code were reversed.

It appears that the browser does not run "innerHTML" code synchronously, but instead creates a new thread for it that executes at its own leisure.

My questions:

  1. What is happening under the hood that is leading to this behavior?
  2. How can I get the browser to behave the way I would expect, that is, force it to update the "innerHTML" before it executes "longPrimeCalc()"?

I tested this in the latest version of chrome.

samayo
  • 16,163
  • 12
  • 91
  • 106
Jonah
  • 15,806
  • 22
  • 87
  • 161
  • `setTimeout(longPrimeCalc, 50);` will solve the issue. And: _" but instead creates a new thread"_ Nope, js has only one active thread. – gdoron May 27 '13 at 15:49
  • `0` instead of `50` is also valid (and more self-explaining). – Grzegorz Rożniecki May 27 '13 at 15:51
  • @Xaerxess, [sort of valid](https://developer.mozilla.org/en-US/docs/Web/API/window.setTimeout#Minimum.2F_maximum_delay_and_timeout_nesting). – gdoron May 27 '13 at 15:53
  • @gdoron I haven't known that but still when I use `setTimeuot` to **immediately** do something in new thread, I'll use 0 ms delay because it shows my intentions (nevermind how each browser implements minimum delay). – Grzegorz Rożniecki May 27 '13 at 16:03
  • 2
    @Xaerxess yes but in this particular case Firefox takes advantage of that and continues to defer the re-flow! – Pointy May 27 '13 at 16:03
  • 3
    @Pointy Oops, that seems like few possible bugs in my codebase :( – Grzegorz Rożniecki May 27 '13 at 16:06
  • @Jonah just tried putting `alert("test");` in between `elm.innerHTML = "thinking"; longPrimeCalc();` and its working as expected. http://jsbin.com/adolur/1/edit – exexzian May 27 '13 at 17:04
  • @Bingo, very interesting. now if there is just some other command we could find that did the same thing without an actual alert.... – Jonah May 27 '13 at 17:22
  • 5
    Should be noted that none of this really has anything to do with `.innerHTML`. You'll get the same behavior for any DOM manipulation where you'd expect a redraw. –  May 27 '13 at 17:25

2 Answers2

9

Your surmise is incorrect. The .innerHTML update does complete synchronously (and the browser most definitely does not create a new thread). The browser simply does not bother to update the window until your code is finished. If you were to interrogate the DOM in some way that required the view to be updated, then the browser would have no choice.

For example, right after you set the innerHTML, add this line:

var sz = elm.clientHeight; // whoops that's not it; hold on ...

edit — I might figure out a way to trick the browser, or it might be impossible; it's certainly true that launching your long computation in a separate event loop will make it work:

setTimeout(longPrimeCalc, 10); // not 0, at least not with Firefox!

A good lesson here is that browsers try hard not to do pointless re-flows of the page layout. If your code had gone off on a prime number vacation and then come back and updated the innerHTML again, the browser would have saved some pointless work. Even if it's not painting an updated layout, browsers still have to figure out what's happened to the DOM in order to provide consistent answers when things like element sizes and positions are interrogated.

gdoron
  • 147,333
  • 58
  • 291
  • 367
Pointy
  • 405,095
  • 59
  • 585
  • 614
  • Have you an example of something that would require the view to be updated? – Martin Smith May 27 '13 at 15:51
  • So how do I interrogate it as you suggest :) I tried putting a `console.log(elm.innerHTML);` in between the two lines of code, but that didn't have any effect. That is, what is the answer my question #2? – Jonah May 27 '13 at 15:53
  • 1
    @MartinSmith I was just looking for something simple; I'll update the answer – Pointy May 27 '13 at 15:53
  • @Jonah answer updated - the key is to ask the browser something that can only be answered if it redoes the layout – Pointy May 27 '13 at 15:54
  • @Pointy, I'm still seeing the same behavior as before :( http://jsbin.com/ezugey/4/edit – Jonah May 27 '13 at 15:56
  • @gdoron yes I know; trying something else now ... sorry about that :-) – Pointy May 27 '13 at 15:57
  • Ha ha - well it's clear that the browser's simply deferring the re-flow, but I haven't found a way to "trick" it into thinking it has to do so in order to be consistent. – Pointy May 27 '13 at 16:01
  • @gdoron it seems that the browser may be recomputing the layout without actually painting it. It's hard to say for sure. – Pointy May 27 '13 at 16:08
  • @Pointy, re: your latest edit, yes that will work but things get pretty hacky quickly. Keep in mind the OP is just a toy to demonstrate the problem. Consider the slightly more real world use of showing "thinking" during the "longPrimeCalc" computation, and returning to orig text when it's done. Now setTimeout must first call "longPrimeCalc", and then some function to return the text to orig. Or even worse I could pass longPrimeCalc a callback. Was really hoping for a better way... – Jonah May 27 '13 at 16:08
  • @Jonah well if you think about it, for most purposes this behavior is a really great idea for page responsiveness. If the browser had to do a re-paint on every DOM update, things would be really slow for lots of very common situations. – Pointy May 27 '13 at 16:10
  • @Pointy, Yeah I can see the reasons for the design as a default, but making me use setTimeout to get around it when needed seems hacky. I think they could at least provide some sort of method like "flush" or "redrawNow" that could be used for situations like this. – Jonah May 27 '13 at 16:13
  • @Jonah, yeah, it's not like js is bad designed or something, there must be a cross browsers solution! – gdoron May 27 '13 at 16:15
  • Yes, I suspect there is no better answer. I will leave the question up for a day or two just in case and then accept this one. – Jonah May 27 '13 at 16:19
  • @Pointy, Apparently even "1" is not safe. Looks like you need at least "2". See the comments in Claudiu's answer... – Jonah May 27 '13 at 16:34
  • @Jonah hmm well 1 worked for me in Firefox (Linux). Browsers enforce a minimum non-zero timeout of 10 or 15 milliseconds in general, and for a real extended computation waiting 15ms for it to start won't really hurt anything. – Pointy May 27 '13 at 16:38
  • @Pointy, I removed the wrapping good for nothing function and change 1 to 10 as the firefox docs recommend. you can rollback (ofcourse) if you dislike the changes. +1 – gdoron May 27 '13 at 20:05
4
  1. I think the way it works is that the currently running code completes first, then all the page updates are done. In this case, calling longPrimeCalc causes more code to be executed, and only when it is done does the page update change.

  2. To fix this you have to have the currently running code terminate, then start the calculation in another context. You can do that with setTimeout. I'm not sure if there's any other way besides that.

Here is a jsfiddle showing the behavior. You don't have to pass a callback to longPrimeCalc, you just have to create another function which does what you want with the return value. Essentially you want to defer the calculation to another "thread" of execution. Writing the code this way makes it obvious what you're doing (Updated again to make it potentially nicer):

function defer(f, callback) {
  var proc = function() {
    result = f();
    if (callback) {
      callback(result);
    }
  }
  setTimeout(proc, 50);
}

function buttonHandler() {
  var elm = document.getElementById("progress");
  elm.innerHTML = "thinking...";
  defer(longPrimeCalc, function (isPrime) {
    if (isPrime) {
      elm.innerHTML = "It was a prime!";
    }
    else {
      elm.innerHTML = "It was not a prime =(";
    }
  });
}
Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • 1
    -1, you added nothing to the existing answer and the comments. – gdoron May 27 '13 at 16:16
  • @gdoron: Jonah said " Consider the slightly more real world use of showing "thinking" during the "longPrimeCalc" computation, and returning to orig text when it's done. Now setTimeout must first call "longPrimeCalc", and then some function to return the text to orig. Or even worse I could pass longPrimeCalc a callback. Was really hoping for a better way..." in his comment. this is a solution that does neither of those things. – Claudiu May 27 '13 at 16:22
  • @Claudiu, I didn't downvote you, but this does not work for me, that is, it does not show "thinking" during the calc. This actually means this setTimeout method won't even work as a hackaround.... So, this does contribute to the discussion. – Jonah May 27 '13 at 16:22
  • @Jonah: oh interesting. I assume you tried the JS fiddle? what browser exactly are you using? – Claudiu May 27 '13 at 16:23
  • @Claudiu, ha, actually it even starts working when you change 1 to 2 – Jonah May 27 '13 at 16:26
  • @Jonah - And on the second click of the button it seems to work when `1` as well. It is only the first click after loading the page that doesn't work for me (on the original fiddle http://jsfiddle.net/Djsfx/3/) – Martin Smith May 27 '13 at 16:28
  • @Jonah: hmm that's strange. Well I re-wrote the code a bit to make it obvious what the timeout is supposed to be doing. – Claudiu May 27 '13 at 16:29
  • the [mozilla specs](https://developer.mozilla.org/en-US/docs/Web/API/window.setTimeout) say: "It's important to note that if the function or code snippet cannot be executed until the thread that called setTimeout() has terminated." which is exactly the desired behavior we're relying on. however, perhaps chrome defines it differently, as this might not be standardized yet... – Claudiu May 27 '13 at 16:37
  • updated again to use a potentially nicer style. take yer pick! – Claudiu May 27 '13 at 16:45