Top Level Scripting

Excerpt

How to implement the code that is required while loading the page and how to avoid blocking.

Implementing JavaScript functions in an simple <script> tag can be seen is many examples in the web as it it quiet simple to do and works in many situations. I also do it here in many examples.

But sometimes it fails and sometimes it blocks.

So here is a short summary about code you have to avoid, about conflicts and side effects within global scripts.

Conflicts in Global Variables

By defining a variable inside a <script> tag the variable is in the global space. Nothing bad with this but is may occur that writing another script is by accident using the same name and the you will have a conflict that is hard to identify.

A possible solution to this is to define variables inside a function scope that makes it availabe to your page scripts but separates them from other scripts and components.

Here is an example that defines a function with inner variables that is instantly called. Everything inside the function behaves mostly exactly the same way but functions and variables are not visible to the outside.

(function () {
  console.info('startup:');

  let lastElementClicked = undefined;

  document.addEventListener("click", (e) => {
    lastElementClicked = e.target;
    console.info("last clicked:", lastElementClicked);
  });
})();

As a side effect encapsulating variables and functions this way also allows a script minifier to used shorter variable names without creating problems as they are not known to the outside.

Long Running tasks - fails

Coding long running things here is freezing the page and must be avoided as demonstrated by the following script:

// blocking count-down script
for (let n1 = 10; n1 >= 0; n1--) {
  for (let sd = Date.now(); Date.now() < sd+1000;) { }
    document.title = `wait for ${n1} secs.`;
    console.info("countdown:", n1);
  }
}

In this script there is no pause implemented as the starting and current time is permanently checked causing HIGH cpu consumption as well.

You can activate this script in this page by a ?countdown parameter to the page url. The title and caption of the browser tab will be changed every second but the page will be visible only after the countdown is over.

Long Running tasks in async functions - fails

There is a "trick" described in some examples on the internet to start a async function that wraps the long running task. However this doesn't work either when there is a blocking script that doesn't use promises or callbacks at all:

// blocking async function
(async function () {
  for (let n1 = 10; n1 >= 0; n1--) {
    for (let sd = Date.now(); Date.now() < sd + 1000;) { }
    console.info("countasync:", n1);
    document.title = `wait for ${n1} secs.`;
  }
})();

You can activate this script in this page by adding a ?countasync parameter to the page url. Again the title and caption of the browser tab will be changed every second but the page will be visible only after the countdown is over.

Top Level Promises - fails

In an global script it is not possible to use the await keyword to resolve promises. The following script will just not work:

// the following code will cause a scripting error:
const r = await fetch('muchdata.json');

You will see an error like:

Uncaught SyntaxError: await is only valid in async functions
    and the top level bodies of modules

The reason for this is quiet obvious: Long running activities are not wanted and thats what a waiting for a Promise result using await in a global script actually is.

So we cant wait, but we can start a asynchronous task. Callbacks and Promises are your friends:

Async functions with callbacks - works

So instead of constantly checking the time it is possible to wait for a timer to call back and wrap this in a Promise. This will allow the JavaScript execution to collaboratively execute multiple threads in parallel:

// Promise and Timer used for waiting
function WaitPromise(milliSeconds) {
  return new Promise(function (resolve, reject) {
    setTimeout(resolve, milliSeconds);
  });
}

if (location.search.includes('counttimer')) {
  (async function () {
    for (let n1 = 10; n1 >= 0; n1--) {
      await WaitPromise(1000);
      console.info("counttimer:", n1);
      document.title = `wait for ${n1} secs.`;
    }
  })();
}

You can activate this script in this page by adding a ?counttimer parameter to the page url. The page will load completely and is not freezing and the countdown in the title will work in parallel.

Wait for the page beeing loaded

However some things don't work as top-level scripts are executed before the document is ready and especially before external scripts and modules are loaded.

As you already have everything wrapped in a function it is just required to control when it is called.

The events from the window object load or DOMContentLoaded can do the trick. So why not delay all top level scripts up to this moment ?

It is the same inner code, but now executed after all documents have been loaded.

if (location.search.includes('countloaded')) {
  window.addEventListener("DOMContentLoaded",
    async (e) => {
      for (let n1 = 10; n1 >= 0; n1--) {
        await WaitPromise(1000);
        console.info("countloaded:", n1);
        document.title = `wait for ${n1} secs.`;
      }
    });
}

You can activate this script in this page by adding a ?countloaded parameter to the page url. You will not see much difference to the previous script but the whole DOM is available and initialized.

Wrapping anything that may run longer or depends on network calls should be embedded in this way into a async function that gets started by the DOMContentLoaded event and avoid any active waiting but used Promises. Using functions like fetch will fit into this as well.

Wait for loaded

However some things don't work as top-level scripts are executed before the document is ready and especially before external scripts and modules are loaded.

As you already have everything wrapped in a function it is just required to controll when it is called.

The events from the window object load or DOMContentLoaded can do the trick. So Why not delay all top level scripts up to this moment ?

It is the same inner code, but now executed after all documents have been loaded.

window.addEventListener("DOMContentLoaded", (e) => {
      console.info('Starting...');
  let lastElementClicked = undefined;
  document.addEventListener("click", (e) => {
    lastElementClicked = e.target;
    console.info("last clicked:", lastElementClicked);
  });
});

See in action...

Use the links:

see the behavior of the blocking and working scripts. You can use the Developer Tools (F12) and have a look to the console output of the scripts you can find inside this page. Happy debugging...

See also

Tags

JavaScript