Async vs Defer: Speed Up JavaScript Loading

Asif SiddiqueAsif Siddique
4 min read

Ever wondered why some websites load at lightning speed while others crawl like a snail? It’s pretty common these days. Building websites has become easier than texting your crush—thanks to modern tools that can spin up an app from just a prompt. But here’s the catch: even after the app is built, there’s still a lot going on behind the scenes. One of the most important factors is speed and optimizations.

When a website loads, the browser first fetches and parses the HTML document. During this process, it also requests the necessary CSS and JavaScript files from the server. Some of these resources are render-blocking—meaning the browser pauses HTML parsing until they’re fully loaded and executed. This delay directly impacts performance, causing lag and slower page loads. In this article, we’ll break down what render-blocking resources are and how to mitigate them using techniques like async and defer.

What happens without async/defer?

Take this example:

<!DOCTYPE html>
<html>
  <head>
    <title>No async/defer</title>
    <script src="./index.mjs"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

Here’s what happens step by step:

  1. HTML parsing starts – The browser begins parsing the HTML from top to bottom.

  2. Script tag found – When it encounters <script src="./index.mjs"></script> without async or defer:

    • Parsing is paused immediately.

    • The browser makes a network request to fetch index.mjs.

  3. Script fetched – Once the script is fully downloaded…

  4. Script executed – The browser runs the JavaScript right away (still no rendering is happening yet).

  5. Resume parsing/rendering – Only after the script has finished executing, the browser continues parsing the rest of the HTML and rendering the page.

async in Action ⚡️

So, what if we add async in the HTML script tag? Let’s have a look!

<!DOCTYPE html>
<html>
  <head>
    <title>No async/defer</title>
    <script async src="./index.mjs"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

When the async attribute is added to a script tag, the script loading and execution flow behaves differently.

  1. HTML parsing starts – The browser begins parsing the HTML from top to bottom.

  2. Script tag found – When it encounters <script async src="./index.mjs"></script> with async:

    • Script download starts immediately in parallel (non-blocking fetch).

    • HTML parsing continues (unlike normal script where parsing pauses).

  3. Script download completes → As soon as the file is ready:

    • Browser pauses parsing to execute the script right away.

    • Important ⚠️: This can happen before, during, or after parsing <body> depending on network speed.

  4. Parsing resumes → After script execution, parsing continues where it left off.

With async, the fetch happens in parallel, but the order of script execution is not guaranteed since each script executes immediately once its download is complete, regardless of the HTML parsing state or other scripts.

defer in Action ⚡️

So, let’s add defer this time in the HTML script tag and see what happens

<!DOCTYPE html>
<html>
  <head>
    <title>No async/defer</title>
    <script defer src="./index.mjs"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

So, If we add defer in the script tag there’s a significant change in the flow:

  1. HTML parsing starts – The browser begins parsing the HTML from top to bottom.

  2. Script tag found – When it encounters <script defer src="./index.mjs"></script> with defer:

    • Script download starts immediately in parallel (non-blocking fetch).

    • Parsing doesn’t pause (unlike the default script).

  3. Parsing continues normally:

    • Meanwhile, the script is downloading in the background.
  4. After parsing finishes (DOM is ready):

    • All deferred scripts are executed in the order they appear in the document (important difference from async, which can fire anytime).
  5. DOMContentLoaded event fires only after deferred scripts are executed:

    • This guarantees your DOM + deferred scripts are ready together.

So, with defer as well fetch happens in parallel but the executing happens after the HTML parsing the order of script execution is guaranteed.

When to use which?

When deciding between async and defer, the rule is simple: use async for scripts that are independent of the DOM and don’t rely on other scripts, such as analytics, ads, or tracking pixels, where execution order doesn’t matter. On the other hand, use defer for scripts that depend on the DOM being parsed or where execution order matters, such as frameworks, libraries, or any code that manipulates page elements. If you don’t specify either attribute, the script will block HTML parsing—meaning the browser will stop rendering, fetch the script, execute it, and then resume parsing—which is rarely ideal in modern applications unless the script is tiny and critical to rendering (like a necessary polyfill).

Conclusion

So, we’ve now seen a brief overview of how async and defer work, along with how script execution interacts with HTML parsing. While a normal script blocks the parser until it’s fetched and executed, async allows scripts to load in parallel and execute as soon as they’re ready, and defer ensures scripts load in parallel but execute only after the DOM has been fully parsed. Choosing between them depends on whether execution order and DOM readiness matter for your scripts.

2
Subscribe to my newsletter

Read articles from Asif Siddique directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Asif Siddique
Asif Siddique