107

I hope I won't make a fool of myself but I'm trying to understand what is happening in those two lines of code:

document.body.innerHTML = 'something';
alert('something else');

What I am observing is that alert shows before HTML has been updated (or maybe it has but the page hasn't been refreshed/repainted/whatever)

Checkout this codepen to see what I mean.

Please note that even putting alert in setTimeout(..., 0) does not help. Looks like it takes more event loops for innerHTML to actually update page.

EDIT:

I forgot to mention I am using Chrome and did not check other browsers. Looks like it's only visible in Chrome. Nevertheless I am still interested why is that happening.

iehrlich
  • 3,572
  • 4
  • 34
  • 43
apieceofbart
  • 2,048
  • 4
  • 22
  • 30
  • 4
    This is typical for Chrome. – trincot Mar 23 '17 at 20:26
  • it worked on firefox – maraca Mar 23 '17 at 20:26
  • 2
    @trincot thanks! I forgot to mention I am using Chrome and did not try other browser before asking. Do you have any reference? – apieceofbart Mar 23 '17 at 20:27
  • Using a `setTimeout` of `1` instead of `0` does what you want -- perhaps Chrome handles the queuing of 0 ms timeouts differently from nonzero timeouts. – apsillers Mar 23 '17 at 20:30
  • 18
    Changing the DOM is synchronous. *Rendering* the DOM actually happens after the JavaScript stack has cleared. https://developers.google.com/web/fundamentals/performance/rendering/ JavaScript > Style > Layout > Paint > Composite. (At least for Chrome. Other browsers are similar.) – jered Mar 23 '17 at 20:30
  • 2
    just a guess: alert being blocking prevents the browser from reaching a repaint step, so while the DOM has changed, it hasn't been allowed to repaint yet; again, just a guess. – zzzzBov Mar 23 '17 at 20:31
  • I would recommend to not use the native alerts at all. If you realize them with an overlay and callback then the alert itself is a change of the dom too. Also many browsers will block native alerts if you once set the checkbox. – maraca Mar 23 '17 at 20:38
  • I'm using Chrome and setTimeout(()=>alert('random'),0) displays the new updated content (I've tried ~10 times) – qbolec Mar 23 '17 at 21:09
  • 2
    @qbolec check this video: https://youtu.be/r8caVE_a5KQ – apieceofbart Mar 24 '17 at 06:42

3 Answers3

138

Setting innerHTML is synchronous, as are most changes you can make to the DOM. However, rendering the webpage is a different story.

(Remember, DOM stands for "Document Object Model". It's just a "model", a representation of data. What the user sees on their screen is a picture of how that model should look. So, changing the model doesn't instantaneously change the picture - it take some time to update.)

Running JavaScript and rendering the webpage actually happen separately. To put it simplistically, first all of the JavaScript on the page runs (from the event loop - check out this excellent video for more detail) and then after that the browser renders any changes to the webpage for the user to see. This is why "blocking" is such a big deal - running computationally intensive code prevents the browser from getting past the "run JS" step and into the "render the page" step, causing the page to freeze or stutter.

Chrome's pipeline looks like this:

enter image description here

As you can see, all of the JavaScript happens first. Then the page gets styled, laid out, painted, and composited - the "render". Not all of this pipeline will execute every frame. It depends on what page elements changed, if any, and how they need to be rerendered.

Note: alert() is also synchronous and executes during the JavaScript step, which is why the alert dialog appears before you see changes to the webpage.

You might now ask "Hold on, what exactly gets run in that 'JavaScript' step in the pipeline? Does all my code run 60 times per second?" The answer is "no", and it goes back to how the JS event loop works. JS code only runs if it's in the stack - from things like event listeners, timeouts, whatever. See previous video (really).

https://developers.google.com/web/fundamentals/performance/rendering/

jered
  • 11,220
  • 2
  • 23
  • 34
  • 1
    thank you for answering. I did know some details about event loop etc. This pipeline makes perfect sense but as examples show it's not the same in every browser. If other browsers wait for the page to repaint before showing alert that means there might be a huge delay between clicking on the button and showing the alert itself. I will try to play with it later. – apieceofbart Mar 24 '17 at 06:49
  • @apieceofbart Other browsers may also simply asynchronously repaint the page while halting the javascript stuff until the user deals with the alert window. It doesn’t mean that they have to wait for the repaint to happen. – Jonas Schäfer Mar 24 '17 at 13:00
  • If painting is asynchronous in major browsers, can I get a callback or Promise when the painting is done? – David Spector Dec 16 '22 at 00:24
  • is it possible to await for the proper moment or handle the repaint event completed? – S. W. G. Apr 29 '23 at 16:12
  • @S.W.G. the best way is probably `requestAnimationFrame()` however its behavior varies across browsers. https://youtu.be/8aGhZQkoFbQ – jered May 01 '23 at 07:57
28

Yes, it is synchronous, because this works (go ahead, type it in your console):

document.body.innerHTML = 'text';
alert(document.body.innerHTML);// you will see a 'text' alert

The reason you see the alert before you see the page changing is that the browser rendering takes more time and isn't as fast as your javascript executing line by line.

d-_-b
  • 21,536
  • 40
  • 150
  • 256
  • Thanks, I did check that. But it has to be something specific to Chrome - it works as expected in other browsers. Or maybe it's their flaw that they wait for page to repaint to move to next line of javascript? – apieceofbart Mar 23 '17 at 20:37
  • 3
    Does the alert show the correct text? (`text` in my example) That will answer your question of if it's synchronous. Browser rendering vs. Javascript execution is apple and oranges :) – d-_-b Mar 23 '17 at 20:38
  • It does but has nothing to do with the question – apieceofbart Mar 23 '17 at 21:01
  • 1
    You question is literally "Is innerHTML asynchronous?". If the value can be used immediately after it is synchronous, no? I think you mean to ask more around page rendering, not the synchronous quality of innerHTML. – d-_-b Mar 23 '17 at 21:08
  • 8
    Yes, the question was clearly wrong but I didn't know what to ask. If I had known the right one if would have found the answer probably. I didn't mean to be mean, thanks for the answer anyway! – apieceofbart Mar 23 '17 at 21:28
  • It's not really about "taking more time" as in duration of execution; it's more about "being performed later" as in scheduling of execution. – Lightness Races in Orbit Mar 24 '17 at 12:57
  • That's correct, but it ignores the rendering part. The model object will update immediately that makes sense, but the actual change to the document is scheduled for rendering, which is asynchronous. The *entire* process of updating `innerHTML` is asynchronous. – adi518 Nov 15 '18 at 18:22
  • If painting is asynchronous in major browsers, can I get a callback or Promise when the painting is done? I need to measure the height of the displayed HTML. – David Spector Dec 16 '22 at 00:25
  • @DavidSpector not exactly but you can utilize the event loop and `requestAnimationFrame` to time your code in sync with browser paints. https://youtu.be/8aGhZQkoFbQ – jered Dec 17 '22 at 05:51
  • Thank you, this works. I will post it here as an answer. – David Spector Dec 18 '22 at 11:53
6

The innerHTML property actual does get updated synchronously, but the visual redraw that this change causes happens asynchronously.

The visual rendering the DOM is asynchronous in Chrome and will not happen until after the current JavaScript function stack has cleared and the browser is free to accept a new event. Other browsers might use separate threads to handle JavaScript code and browser rendering, or they might let some events get priority while an alert is halting the execution of another event.

You can see this in two ways:

  1. If you add for(var i=0; i<1000000; i++) { } before your alert, you've given the browser plenty of time to do a redraw, but it hasn't, because the function stack has not cleared (add is still running).

  2. If you delay your alert via an asynchronous setTimeout(function() { alert('random'); }, 1), the redraw process will get to go ahead of the function delayed by setTimeout.

    • This does not work if you use a timeout of 0, possibly because Chrome gives event-queue priority to 0 timeouts ahead of any other events (or at least ahead of redraw events).
apsillers
  • 112,806
  • 17
  • 235
  • 239
  • Thank you for answering! Please not that even `setTimeout(func, 1)` did not work every time, check this video: https://youtu.be/r8caVE_a5KQ – apieceofbart Mar 24 '17 at 06:43