Scripts: async and defer
How you load your scripts impacts page performance. The async and defer attributes give you control over when scripts download and execute.
The Problem
A regular <script> tag blocks HTML parsing while it downloads and executes:
<script src="heavy.js"></script>
<!-- HTML parsing stops here until heavy.js downloads and runs -->
This is why putting scripts in <head> without defer or async slows down page load.
The Three Modes
| Attribute | Download | Execution | Order |
|---|---|---|---|
| (none) | Blocks HTML parsing | Immediately when downloaded | In document order |
async | Non-blocking | As soon as downloaded | No guarantee |
defer | Non-blocking | After HTML parsed, before DOMContentLoaded | In document order |
Normal: HTML βββββββ€ script βββββββ HTML βββββββΆ
Async: HTML βββββββββββββ€ script ββββΆ
(script downloads in parallel)
Defer: HTML βββββββββββββββββββββ€ script ββββΆ
(script runs after HTML parsed, before DOMContentLoaded)
defer β Safe, Ordered Execution
defer tells the browser: βDownload the script, but wait until HTML is parsed to execute it.β
<script defer src="analytics.js"></script>
<script defer src="app.js"></script>
- Both download in parallel during HTML parsing
- Both execute in order (analytics.js first, then app.js)
- Both execute before
DOMContentLoaded
Use defer for scripts that need the DOM and depend on each other.
async β Independent Execution
async tells the browser: βDownload as soon as possible, run as soon as downloaded.β
<script async src="analytics.js"></script>
<script async src="widget.js"></script>
- Both download in parallel
- Each executes immediately when downloaded
- Order is not guaranteed (analytics may run after widget)
Use async for independent scripts (analytics, ads, counters) that donβt depend on the DOM or other scripts.
Visual Comparison
Normal script in <head>:
Parsing βββ€scriptββββΆ DOMContentLoaded βββΆ load
(blocked)
Deferred script:
Parsing βββββββββββββββββΆ DOMContentLoaded βββΆ load
βββdeferβββΆ (after parse)
Async script:
Parsing βββββββΆ DOMContentLoaded βββΆ load
βasyncβ€ (when ready)
When to Use Which
| Scenario | Best Choice |
|---|---|
| Script uses DOM APIs | defer |
| Script depends on another script | defer (order preserved) |
| Analytics / tracking | async |
| Third-party widgets | async |
| Small inline script | Inline at end of <body> |
| Script doesnβt need DOM | async |
Dynamic Script Loading
You can also load scripts dynamically:
const script = document.createElement('script');
script.src = 'app.js';
script.async = true; // or false for defer-like behavior
document.head.appendChild(script);
By default, dynamically created scripts behave like async (they donβt block, but execute as soon as downloaded). Set script.async = false to make them behave like defer (ordered, but still execute after DOM).
Detecting Script Load
Use the load event on script elements:
script.onload = function() {
console.log('Script loaded!');
};
script.onerror = function() {
console.error('Failed to load script');
};
<div id='dynamic-script-demo'>
<p><button id='load-script'>Load external script dynamically</button></p>
<pre id='script-log' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
</div> const log = document.getElementById('script-log');
document.getElementById('load-script').onclick = function() {
const script = document.createElement('script');
script.src = 'data:text/javascript,' + encodeURIComponent(
'document.getElementById('script-log').textContent += 'Dynamic script executed!' + new Date().toLocaleTimeString() + '\n;'
);
script.async = true;
script.onload = function() {
log.textContent += 'Script load event fired\\n';
this.disabled = true;
this.textContent = 'Loaded';
};
script.onerror = function() {
log.textContent = 'Script failed to load';
};
document.head.appendChild(script);
log.textContent = 'Script element created and appended\\n';
}; Module Scripts
For modern JavaScript (ES modules), the type="module" attribute behaves like defer by default:
<script type="module" src="app.js"></script>
Module scripts:
- Are deferred by default (download in parallel, execute after parse)
- Can use
importandexport - Strict mode by default
- Execute only once (even if included multiple times)
Key Takeaways
- Regular scripts block HTML parsing β avoid them in
<head> defer= download in parallel, execute in order after DOM readyasync= download in parallel, execute immediately when ready- Use
deferfor your code (needs DOM, depends on other scripts) - Use
asyncfor third-party scripts (analytics, widgets) - Dynamic scripts behave like
asyncby default type="module"behaves likedeferautomatically