14

While trying to answer this question, I met a weird behavior (which isn't the same: his is due to too few iterations, mine to too much):

HTML:

<button id="go">it will be legend...</button>
<div id="output"></div>

JS:

var output = document.getElementById('output');
document.getElementById('go').onclick = function() {
    output.textContent += 'wait for it...';
    for (var i=0; i<3000000000; i++) {
        var unused = i; // don't really care
    }
    output.textContent += ' dary!';
};

The loop takes few seconds to execute, because of its 3,000,000,000 iterations.

Once the button is clicked, what I expected:

  1. wait for it... appears
  2. the process freezes a little bit because of the loop
  3. dary! appears

What actually happened:

  1. the process freezes a little bit because of the loop
  2. wait for it... dary! appears together

Any idea why such a behavior?

Check by yourself: fiddle.

Community
  • 1
  • 1
sp00m
  • 47,968
  • 31
  • 142
  • 252
  • 1
    Does it behave this way in all browsers? Works correctly if you set a small timeout – Huangism Aug 20 '13 at 16:08
  • I saw the fiddle, that's really weird ....and i thought `looping` is a plain-vanilla process ! – The Dark Knight Aug 20 '13 at 16:12
  • 1
    Also browsers optimize changes to the DOM/CSS so that they are only executed when necessary (to avoid multiple consecutive reflows). – Felix Kling Aug 20 '13 at 16:15
  • 1
    My guess is the setting textContent is asynchronous and while the browser is trying to display the text, the 3 bilion loop is freezing the browser so you won't see any updates until it is done, but by the time it is done the text is set again. Here is the same fiddle with timeout http://jsfiddle.net/2MrD8/2/ – Huangism Aug 20 '13 at 16:17

5 Answers5

17

The reason is the function, as a whole, is executing synchronously. By the time you set the output to wait for it..., it enters the long-running loop and hog the thread. If you wrap the rest in a timeout, the first text will appear as normal.

var output = document.getElementById('output');
document.getElementById('go').onclick = function() {
    output.textContent += 'wait for it...';
    window.setTimeout(function() {
    for (var i=0; i<3000000000; i++) {
        var unused = i; // don't really care
    }
    output.textContent += ' dary!';
    }, 0);
};

Note that it will still freeze the UI while processing.

Edit: Using 0 as the delay value in Chrome works, but it does not in latest Firefox and IE 10. Changing the value to 10 works in both cases.

Simon Belanger
  • 14,752
  • 3
  • 41
  • 35
  • I ran this in fiddle, it is still printing `wait for it...dary!` together! – Sid Aug 20 '13 at 16:20
  • @Sid I tested it in fiddle and had the proper behavior (using chrome). [JSFiddle](http://jsfiddle.net/2MrD8/3/). Edit: Just tested in firefox and it did show it together. if you change the `0` to `10` it works as expected. Looks like firefox is executing it synchronously in some cases. – Simon Belanger Aug 20 '13 at 16:21
  • You might want to edit this line `output.textContent += ' dary!'; }, 0);` I set it to 100 and now it is working properly as expected. check this [fiddle](http://jsfiddle.net/2MrD8/4/) – Sid Aug 20 '13 at 16:23
  • @Sid Yes, I just tested it. Although it worked with a value of 10 in firefox. – Simon Belanger Aug 20 '13 at 16:24
  • Wouldn't it be synchronous if it showed `wait for it...` first and not together. What happened in the question looks asynchronous to me i.e. not showing `wait for it...` until loop finishes and then showing it together with `dary!`. – user568109 Aug 20 '13 at 17:47
  • @user568109: Not quite. The browser is what's running stuff synchronously here. It typically passes control to JS and then *waits for the script to return* before tending to the visual side effects it caused. (I'm not sure whether it's a hard-set rule, but it's what most if not all browsers do.) The changes the script is making are visible to it immediately, but won't be visible to the user til the browser gets control back and goes back to handling UI messages. – cHao Aug 20 '13 at 18:42
  • Note that it'd be better for the user if `setTimeout(function() { /* tie up the CPU for ages, then output */ }, 0);` were replaced with `setTimeout(function() { /* output immediately */ }, 15000);` (or some other appropriate timeout value). This returns immediately, so the first message appears, *and* avoids the busy-wait that makes the browser look like it's locked up. – cHao Aug 20 '13 at 18:46
10

Javascript is pretty much single-threaded. If you're running code, the page is non-responsive and will not be updated until your code has completed. (Note that this is implementation specific, but this is how all browsers do it today.)

Dark Falcon
  • 43,592
  • 5
  • 83
  • 98
2

Dark Falcon and Simon Belanger have provided explanations for the cause; this post discusses a different solution. However, this solution is definitely NOT appropriate for a 3-billion iteration loop as it is too slow by comparison.

According to this SO post by user Cocco, using setTimeout is less optimal than requestAnimationFrame for this purpose. So, here's how to use requestAnimationFrame:

jsFiddle example

$(document).ready(function() {
    var W = window,
        D = W.document,
        i = 0,
        x = 0,
        output = D.getElementById('output');

    function b() {
        if (x == 0) {
            output.textContent = 'wait for it...';
            x++;
        }
        i++;
        if (i < 300) {
            //if (i > 20) output.textContent = i;
            requestAnimationFrame(b);
        } else {
            D.body.style.cursor = 'default';
            output.textContent += ' dary!';
        }
    }

    function start() {
        console.log(D)
        D.body.style.cursor = 'wait';
        b();
    }
    D.getElementById('go').onclick = start;
}); //END $(document).ready()
halfer
  • 19,824
  • 17
  • 99
  • 186
cssyphus
  • 37,875
  • 18
  • 96
  • 111
1

Your code is executing as you expect it. The issue is that the browser won't display your change to the document, until after the javascript is done. The time out is fixing this issue, by breaking the code execution into two separate events. The following code will show that what you expected is happening.

var output = document.getElementById('output');
document.getElementById('go').onclick = function() {
    console.log('wait for it...';)
    for (var i=0; i<3000000000; i++) {
        var unused = i; // don't really care
    }
    console.log(' dary!');
};

You also need to be careful when using the timeout solution, since execution is no longer synchronous.

output = document.getElementById('output');
document.getElementById('go').onclick = function() {
    output.textContent += 'wait for it...';
    window.setTimeout(function() {
        for (var i = 0; i < 3000000000; i++) {
        var unused = i;
        // don't really care
        }
    output.textContent += ' dary!';
    }, 0);
    output.textContent += ' epic';
};

If you run this version, you will notice that ' epic' is before ' dary'.

Philip Tinney
  • 1,986
  • 17
  • 19
0

The only explanation I see is that the browser refreshes the view after the javascript is executed ? As a proof, this works as expected :

var output = document.getElementById('output');
document.getElementById('go').onclick = function () {
    output.textContent += 'wait for it...';
    window.setTimeout(count, 100);
};

function count() {
    for (var i = 0; i < 3000000000; i++) {
        var unused = i; // don't really care
    }
    output.textContent += ' dary!';
}
Julien
  • 2,544
  • 1
  • 20
  • 25