When it comes to web development, the way we load and execute JavaScript can have a significant impact on the performance and user experience of our website.

When the browser loads HTML and comes across a <script>...</script> tag, it can’t continue building the DOM. It must execute the script right now. The same happens for external scripts <script src="..."></script>: the browser must wait for the script to download, execute the downloaded script, and only then can it process the rest of the page.

That leads to two important issues:

  1. Scripts can’t see DOM elements below them, so they can’t add handlers etc.
  2. If there’s a bulky script at the top of the page, it “blocks the page”. Users can’t see the page content till it downloads and runs:
<p>...content before script...</p>

<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- This isn't visible until the script loads -->
<p>...content after script...</p>

There are some workarounds to that. For instance, we can put a script at the bottom of the page. Then it can see elements above it, and it doesn’t block the page content from showing:

<body>
  ...all content is above the script...

  <script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>

PS: But this solution is far from perfect. For example, the browser notices the script (and can start downloading it) only after it downloaded the full HTML document. For long HTML documents, that may be a noticeable delay.

In other words:

Such things are invisible for people using very fast connections, but many people in the world still have slow internet speeds and use a far-from-perfect mobile internet connection.

This is where script attributes come into play, and in this article, we’ll dive into the world of async and defer, two powerful tools to optimize our JavaScript loading.

PS: async and defer are HTML attributes that we can apply to our tags to control how the browser handles the loading and execution of our JavaScript files.

defer

The defer attribute tells the browser not to wait for the script. Instead, the browser will continue to process the HTML, build DOM. The script loads “in the background”, and then runs when the DOM is fully built.

Here’s the same example as above, but with defer:

<p>...content before script...</p>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- visible immediately -->
<p>...content after script...</p>
  • Scripts with defer never block the page.
  • Scripts with defer always execute when the DOM is ready (but before DOMContentLoaded event).

The following example demonstrates the second part:

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<p>...content after scripts...</p>
  1. The page content shows up immediately.
  2. DOMContentLoaded event handler waits for the deferred script. It only triggers when the script is downloaded and executed.

The defer attribute tells the browser to load the script in parallel with the HTML parsing, but to execute the script only after the HTML parsing is complete. This ensures that the script is executed in the order they appear in the HTML, which is important for scripts that have dependencies on each other or on the DOM structure.

Let’s say, we have two deferred scripts: the long.js and then small.js:

<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>

Browsers scan the page for scripts and download them in parallel, to improve performance. So in the example above both scripts download in parallel. The small.js probably finishes first.

…But the defer attribute, besides telling the browser “not to block”, ensures that the relative order is kept. So even though small.js loads first, it still waits and runs after long.js executes.

This is useful for scripts that need to access the DOM or rely on other scripts being loaded first, such as our application’s main JavaScript file.

async

The async attribute is somewhat like defer. It also makes the script non-blocking. But it has important differences in the behavior.

The async attribute tells the browser to load the script asynchronously, meaning the script will be downloaded in parallel with the parsing of the HTML, and it will be executed as soon as it’s available, without blocking the HTML parsing.

This is particularly useful for scripts that are independent of the initial page content and don’t need to wait for the DOM to be fully loaded, or any script that doesn’t have a direct dependency on the page’s content.

In other words, async scripts load in the background and run when ready. The DOM and other scripts don’t wait for them, and they don’t wait for anything.

  1. The browser doesn’t block on async scripts (like defer).
  2. Other scripts don’t wait for async scripts, and async scripts don’t wait for them.
  3. DOMContentLoaded and async scripts don’t wait for each other:

Here’s an example similar to what we’ve seen with defer: two scripts long.js and small.js, but now with async instead of defer.

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>

<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>

<p>...content after scripts...</p>
  • The page content shows up immediately: async doesn’t block it.
  • DOMContentLoaded may happen both before and after async, no guarantees here.
  • A smaller script small.js goes second, but probably loads before long.js, so small.js runs first. Although, it might be that long.js loads first, if cached, then it runs first. In other words, async scripts run in the “load-first” order.

Async scripts are great when we integrate an independent third-party script into the page: counters, ads and so on, as they don’t depend on our scripts, and our scripts shouldn’t wait for them:

<!-- Google Analytics is usually added like this -->
<script async src="https://google-analytics.com/analytics.js"></script>

PS: The defer,async attributes are only for external scripts. The attribute are ignored if the <script> tag has no src.

Dynamic scripts

There’s one more important way of adding a script to the page.

We can create a script and append it to the document dynamically using JavaScript:

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

The script starts loading as soon as it’s appended to the document (*).

Dynamic scripts behave as “async” by default.

This can be changed if we explicitly set script.async=false. Then scripts will be executed in the document order, just like defer.

In this example, loadScript(src) function adds a script and also sets async to false.

So long.js always runs first (as it’s added first):

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// long.js runs first because of async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");

Without script.async=false, scripts would execute in default, load-first order (the small.js probably first).

References:

https://javascript.info/

By Shabazz

Software Engineer, MCSD, Web developer & Angular specialist , BizDevOps

Leave a Reply

Your email address will not be published. Required fields are marked *