javascripthtmlbrowserrenderrepaint

HTML/JS: How to prevent a render before script execution?


I've just noticed strange behaviour with rendering before script is being performed. I always thought, that there is no chance for browsers to dequeue a task from render queue and execute it before script tag, since this is a single thread. And as you will see, it's not true. There is a simple example of code below:

console.log('SCRIPT EXECUTED')
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>HTML</title>

    <script>
        requestAnimationFrame(function animate(time) {
            console.log('RAF', performance.now());
        });
    </script>
</head>

<body>
<h1>Hello world</h1>

<script src="./script.js"></script>
</body>
</html>

One may see inconsistent output in console. Sometimes render comes up before script and vice versa. It may cause to flickering in UI for example.

Could anyone give a meaningful explanations of this?


Solution

  • External classic scripts are parser-blocking, i.e. the HTML parser will wait until the script's execution before continuing parsing the remaining of the document.

    <script src="data:text/javascript,console.log(document.body.children[document.body.children.length - 1].nodeName);"></script>
    <span>Should log SCRIPT and not SPAN</span>

    But to also be render-blocking, they need to be in the <head> of the document.

    /*
     * Since StackSnippets do wrap the HTML & JS content in the <body>,
     * we use another <iframe> for our demo.
     * But StackSnippet's console isn't available there,
     * so we use postMessage and log from here.
     */
    onmessage = ({data}) => console.log(...data);
    <iframe srcdoc="
    <!DOCTYPE HTML>
    <html>
      <head>
        <script>
          requestAnimationFrame(function animate(time) {
            parent.postMessage(['RAF', performance.now()], '*');
          });
        </script>
        <script src='./script'></script>
        <script>
          parent.postMessage(['SCRIPT EXECUTED'], '*')
        </script>
      </head>
      <body>
        <h1>HELLO WORLD</h1>
      </body>
    </html>"></iframe>

    Note that Firefox apparently doesn't support render-blocking yet.

    Also, you may be interested in the blocking="render" attribute, which allows a <script> to not be parser blocking (defer, async, or module) while being render-blocking, though this is currently only available for external scripts, but this might change in the near future.

    onmessage = ({data}) => console.log(...data);
    /* In Chrome this logs
     * SCRIPT EXECUTED
     * H1
     * "RAF", a number
     */
    <iframe srcdoc="
    <!DOCTYPE HTML>
    <html>
      <head>
        <script>
          requestAnimationFrame(function animate(time) {
            parent.postMessage(['RAF', performance.now()], '*');
          });
        </script>
        <script defer src='data:text/javascript,parent.postMessage([document.body.children[document.body.children.length - 1].nodeName], `*`);'></script>
        <script>
          parent.postMessage(['SCRIPT EXECUTED'], '*')
        </script>
      <head>
      <body>
        <h1>HELLO WORLD</h1>
      </body>
    </html>"></iframe>