Home > Net >  Order of execution with setTimeout with multiple scripts?
Order of execution with setTimeout with multiple scripts?

Time:01-19

I have an empty html page with 2 scripts:

  <script src="./scripta.js"></script>
  <script src="./scriptb.js"></script>

scripta.js:

console.log("from a")

setTimeout(() => {
  console.log("from a timeout")
}, 0)

scriptb.js

console.log("from b")

Now, when I refresh the page I mostly see those logs in my console:

from a
from b
from a timeout

That's how it's supposed to be from my understanding.

However, after reloading page multiple times I've noticed those logs too:

from a
from a timeout
from b

Can someone explain or point me to an answer why I see the second variation of logs? Thanks.

CodePudding user response:

As a preface, remember that when browsers encounter a plain ol' script element, such as <script>/* some script */</script> or <script src="someScript.js"></script> then the browser immediately runs (or downloads-and-runs) the script - whereas if the <script> has a src="" attribute and has the defer or async attributes then the execution of the script will be delayed.

  • (As a parenthetical: I do think it's silly that we can't use defer with inline scripts, though the workaround is just to wrap your code in a DOMContentLoaded event-listener callback).

While the spec for setTimeout does guarantee some ordering of callbacks, it does not require that a browser run func immediately after a script with setTimeout( func, 0 ) has completed.

As per my understanding of the HTML DOM specs, the following two scenarios should be considered roughly equivalent and should always have the same results (assuming everything's cached) and have the following output:

foo
bar
myCallback

Example 1

<html>
<head>
  <script>
    console.log("foo");
    window.setTimeout( myCallback, 0 );
    function myCallback() {
      console.log("myCallback");
    }
  </script>
  <script>
    console.log("bar");
  </script>
</head>
</html>

Example 2

(Where scriptA.js and scriptB.js have the same script-bodies as their respective inline <script> in the previous example)

<html>
<head>
  <script src="scriptA.js"></script>
  <script src="scriptB.js"></script>
</head>
</html>

In your case, the following is happening:

  1. Browser downloads the page's HTML and begins parsing it.

  2. When the browser encounters the first <script src="scriptA.js"></script> then it will pause processing the rest of the HTML until it runs scriptA.js.

  3. The browser runs scriptA.js which:

    3.1. Calls console.log("from a").

    3.2. The anonymous function (the => fat-arrow-function that's syntactically inside the setTimeout call-site) is added to the browser's Task queue for execution for the next event-cycle, which generally happens when the browser feels like it - i.e. it is not deterministic:

    • A delay value of 0 does not mean "execute immediately" nor does it mean "execute concurrently", instead it specifically means "execute in next event-cycle".
    • JavaScript in the browser is always single-threaded: concurrent execution is only possible via service-workers.

    3.3. After scriptA.js completes, the browser then is free to choose what to do next depending on runtime conditions (read on).

  4. Optimally, assuming that the second <script> element's HTML text is already downloaded (which is almost always the case, as it's not like each HTML element is sent in its own HTTP/TCP packet) then the browser's HTML parser will then see it needs to download and run scriptB.js - and if scriptB.js is already cached in your browser, then the browser can execute it immediately, which means that scriptB.js will run before scriptA.js's setTimeout callback.

  5. However if scriptB.js is not already downloaded (cached, parsed and ready to execute) then the browser knows it has to wait for that first, which introduces a blocking wait, but browsers can still use that blocking wait to run any pending JavaScript Tasks, including those added by setTimeout.


  •  Tags:  
  • Related