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.
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
- 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. - "Scripts at the bottom of body" is deprecated: This was a workaround for the halting behavior. Use
deferinstead. defer: Download in parallel with HTML parsing. Execute after parsing is fully complete. Safe for DOM access. Use this as your default.async: Download in parallel. Execute as soon as the download finishes, even if parsing is still ongoing. Best for independent scripts like analytics.type="module"is deferred automatically: Modules already behave likedeferwithout 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
<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:
<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
- MDN: script element - Full reference for all script tag attributes including
defer,async, andtype="module". - MDN: JavaScript modules - Guide to ES module loading and how it differs from classic scripts.
Video:
- Async vs Defer in JavaScript by Akshay Saini. A clear visual explanation of how each loading mode affects the parsing timeline.