Async vs Defer Script Attribute: Loading Modes Compared
Compare how browsers handle scripts with no attribute, async, and defer. Learn when to use each mode, module defaults, and placement rules.
Async vs Defer Script Attribute: Loading Modes Compared
How you load JavaScript changes when it downloads, when it runs, and how long the browser blocks HTML parsing. The three modes below produce very different Core Web Vitals results even when the file itself is identical.
The three loading modes at a glance
| Mode | Download | Execution | Blocks parser | Order preserved |
|---|---|---|---|---|
<script> | Immediate, blocking | Immediately after download | Yes (both fetch + run) | Yes |
<script async> | Parallel with HTML | As soon as ready | Only during execution | No |
<script defer> | Parallel with HTML | After HTML parsing, before DOMContentLoaded | Never | Yes |
Timeline diagram
Legend: [H] HTML parsing [D] JS download [E] JS execution
No attribute (classic):
[H H H][D D D D][E E E][H H H H H H]
^ parser fully halted while script downloads AND runs
async:
[H H H H H H H H][E E E][H H H]
[ D D D D D D ]
^ downloads alongside HTML; execution stops parsing whenever it lands
defer:
[H H H H H H H H H H H H][E E E]
[ D D D D D D ]
^ downloads alongside HTML; execution waits until parsing finishes1. No attribute: block and run
A plain <script src="app.js"> pauses HTML parsing the moment the browser hits it. Nothing after the tag renders until the file is fetched, parsed, and executed. This is why the old advice was to drop scripts just before </body>: any parser-blocking script above the fold delays First Contentful Paint and Largest Contentful Paint.
2. async: fire and forget
With async, the browser fetches the script in parallel with HTML parsing, then interrupts parsing to execute the moment the download finishes. Order is not guaranteed: if you load a.js then b.js and b.js arrives first, b.js runs first. Use async for scripts that do not depend on the DOM or on each other: analytics beacons, error trackers, ad pixels, and third-party widgets that self-contain.
3. defer: parallel download, ordered execution
defer also downloads in parallel, but execution waits until the HTML is fully parsed and runs in the order the tags appear. Deferred scripts fire right before DOMContentLoaded, so the DOM is guaranteed to exist. Use defer for application code, framework bootstraps, and anything that queries or mutates the DOM or relies on a helper loaded in an earlier tag.
type="module" is deferred by default
ES modules (<script type="module">) behave like defer automatically: they download in parallel and execute after parsing, in document order. You do not add defer to a module, and adding async switches it to async semantics. Classic scripts have no default: without async or defer, they block.
Placement: the old rule is obsolete
Because defer and modules never block the parser, you can put them back in <head> where they belong. Putting a deferred script in the head actually helps performance: the browser discovers it earlier and starts the download sooner, but execution still waits for the DOM. The "always at end of body" rule only applies to legacy parser-blocking scripts.
Which one should you use?
- Analytics, pixels, independent third-party scripts:
async. Order does not matter; earliest execution wins. - App bundles, jQuery plugins, anything touching the DOM or depending on another file:
defer. - Inline critical JavaScript (feature flags, theme detection): no attribute, kept tiny, in the head. This is the one case where blocking is acceptable.
- Modern ES modules:
type="module"and let the default defer behavior handle it.
Common mistakes
- Marking a jQuery-dependent plugin as
asyncand getting "$ is not defined" race conditions. - Adding
deferto an inline<script>withoutsrc. The attribute is ignored on inline scripts (except modules). - Using both
asyncanddefer. Older browsers useddeferas a fallback; modern browsers preferasync, so ordering is lost. - Leaving render-blocking scripts in the head and blaming a poor LCP on the CDN.