Async Defer Script Loading

The default script tag halts HTML parsing. "Put scripts at the bottom" is a deprecated workaround. Here is the modern answer with async and defer.

May 1, 20263 min read6 / 12

Before adding JavaScript to the project, there is one configuration choice on the script tag itself that matters for performance. It is easy to get wrong, and a lot of older tutorials get it wrong in a way that is now considered bad practice.

The Essentials

  1. Default script loading halts parsing: When the browser encounters a <script src="..."> tag while parsing HTML, it stops, downloads the file, executes it, then resumes parsing. This causes a blank screen delay.
  2. "Scripts at the bottom of body" is deprecated: This was a workaround for the halting behavior. Use defer instead.
  3. defer: Download in parallel with HTML parsing. Execute after parsing is fully complete. Safe for DOM access. Use this as your default.
  4. async: Download in parallel. Execute as soon as the download finishes, even if parsing is still ongoing. Best for independent scripts like analytics.
  5. type="module" is deferred automatically: Modules already behave like defer without explicitly writing the attribute.

Why the Blank Screen Happens

When a browser parses an HTML file, it works top to bottom. When it hits a <script> tag with a src attribute, it stops everything: pauses the HTML parser, downloads the JavaScript file, executes it from top to bottom, then resumes parsing the rest of the page.

If your script is large or slow to download, the user sees nothing on screen until that completes. This is the original source of the white screen problem.

The old answer was: move your script tags to the very bottom of <body>. That way the browser parses and renders all the HTML before hitting the script tag, so the user sees the page before JavaScript runs. It works, but it is a workaround, not a solution.

The Modern Solution: defer

HTML
<script src="app.js" defer></script>

defer changes the behavior: the browser downloads the JavaScript file in parallel while it continues parsing the HTML. It does not execute the script until parsing is complete. The result is:

  • No parsing halt while downloading
  • DOM is fully parsed before the script runs
  • You can safely access DOM elements at the top level of your script

This makes "scripts at the bottom" unnecessary. Put the <script> tag wherever makes semantic sense in the HTML, usually in the <head>, and let defer handle the timing.

When to Use async Instead

async also downloads in parallel, but it executes the script as soon as the download completes, regardless of whether the HTML parser has finished. This means:

  • Fast execution for scripts that do not need the DOM
  • Unpredictable execution order if you have multiple async scripts
  • Risk of errors if the script tries to access DOM elements that have not been parsed yet

async is well suited for third-party analytics scripts, chat widgets, and other tools that are completely independent of your page's content and do not need to touch the DOM.

A simple decision rule: if your script needs to access the DOM, use defer. If it is completely standalone and fast to execute, async is fine.

type="module" Already Implies defer

If you add type="module" to your script tag, the module system is inherently deferred. You do not need to also write defer:

HTML
<script type="module" src="app.js"></script>

This script downloads in parallel, executes after parsing, and variables inside it are scoped to the module rather than being global. This is the pattern used in modern vanilla JS projects that organize code into separate files.

Further Reading and Watching

Video: