2

Dear stackoverflow community,

This is a very simplified version of my problem.

I wrote a span in my HTML's body and I set the HTML's JavaScript to change the span's text before processing a given task.

JavaScript however decides not to change the text of the span before processing the function FOR(). Instead it changes the span's text after it's done doing the given task. Completely ignoring the fact that the command to change the span's text is positioned one line above the command to do the processing task.

Why is that and how can I fix this? (I want the span's text to be changed before JavaScript is starting to process the task)

<html>
  <body>
    <button onclick="pressed()">press here</button>
    <span id="text">still the same text</span>
    <script>
        function FOR(rounds) 
        {
          var x = 0;
          for(var i=0; i<rounds; i++)
          {
              x+=Math.sqrt(i);
          }

          return(x);
        }

        function pressed() 
        {
          var text = document.getElementById("text");
          text.innerHTML = "i have changed";

          console.log(FOR(2000000000));
          console.log("processing done");
        }
    </script>
  </body>
</html>
Guerric P
  • 30,447
  • 6
  • 48
  • 86
53N71N3L
  • 23
  • 5

2 Answers2

2

That's because the browser waits for the current callstack (functions calling each other) to completely unwind before applying any change to the rendered page. In order to solve this problem, you have to defer the second function in the callback queue with a setTimeout or queueMicrotask. You can try running this with the dev tools opened and you'll notice that the span has changed when the breakpoint is reached:

<html>
    <body>
        <button onclick="pressed()">press here</button>
        <span id="text">still the same text</span>
        <script>
            function FOR(rounds) {
                var x = 0;
                for (var i = 0; i < rounds; i++) {
                    x += Math.sqrt(i);
                }
            
                return (x);
            }
            
            function pressed() {
                var text = document.getElementById("text");
                text.innerHTML = "i have changed";
            
                queueMicrotask(function () {
                    debugger;
                    console.log(FOR(2000000000));
                    console.log("processing done");
                });
            }
        </script>
    </body>
</html>
Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • Even without the queueMicroTask you would see the updated text while debugger is active... queueMicroTask won't help at all here, and setTimeout might only randomly. – Kaiido Sep 16 '20 at 23:02
1

The text is changed in the DOM, synchronously, but the rendering to screen will be done only in a particular event loop iteration (hereafter "painting frame") where the "update the rendering" steps will be called.

Blocking synchronously the task will not allow the browser to reach this part of the event loop, and thus no rendering will occur in between.

You could wait for the next painting frame to have fired before calling your blocking script:

function FOR(rounds) {
    var x = 0;
    for (var i = 0; i < rounds; i++) {
        x += Math.sqrt(i);
    }
    return (x);
}

function pressed() {
    var text = document.getElementById("text");
    text.innerHTML = "i have changed";
    // requestAnimationFrame schedules a callback to fire before the next rendering
    requestAnimationFrame( () => {
      // so we still need to wait the next iteration of the event loop
      setTimeout(() => {
        console.log(FOR(2000000000));
        console.log("processing done");
      },0);
    });
}
<button onclick="pressed()">press here</button>
<span id="text">still the same text</span>

But the best is to not lock your UI at all.

If you have heavy computations to be done, then use a Web-Worker, which will allow your browser to use an other thread to perform the computations, leaving the main UI thread free to do its main work:

// StackSnippet can't fetch external files, so here we have to build it
// in your case you'd just have it in an external file
const worker_script  = `
function FOR(rounds) {
    var x = 0;
    for (var i = 0; i < rounds; i++) {
        x += Math.sqrt(i);
    }
    return (x);
}
self.onmessage = (evt) => {
  // post back the result
  self.postMessage( FOR( evt.data ) );
}`;
const worker_blob = new Blob([worker_script],{type:"text/javascript"});
const worker_url = URL.createObjectURL(worker_blob);

// initialise the worker
const worker = new Worker(worker_url);
const button = document.querySelector("button");
function pressed() {
  var text = document.getElementById("text");
  text.innerHTML = "i have changed " + new Date().getTime();
  // handle the results from the worker
  worker.addEventListener("message", (evt) => {
      console.log(evt.data);
      console.log("processing done");
      button.disabled = false;
  }, {once: true} );
  // start the computation
  worker.postMessage(2000000000);
  button.disabled = true;
  console.log("new computation started");
}
<button onclick="pressed()">press here</button>
<span id="text">still the same text</span>

However beware that waiting for just a single timeout or even worse for microtask, won't do. Using setTimeout you can't be sure that the paiting frame (which normally occurs at the same frequency as the monitor's refresh rate, i.e every 16.6ms in most general cases), will occur in between. Using microtasks, you can be sure it won't have occuredAny example proving microtask is executed before rendering?.

Kaiido
  • 123,334
  • 13
  • 219
  • 285