Skip to main content

defer vs async in a script tag

defer and async are <script> attributes that remove the blocking of HTML parsing: both download the file in parallel with the markup, but defer runs the scripts after the DOM is built and in declaration order, while async runs each script immediately after it downloads, in an arbitrary order. That moment of execution is the entire difference between them.

Theory

TL;DR

  • With no attributes, <script src> halts HTML parsing, which blocks rendering.
  • defer: parallel download, execution after the HTML is fully parsed, order preserved.
  • async: parallel download, execution right after the download, order not guaranteed.
  • defer scripts run before DOMContentLoaded and the event waits for them; async does not.
  • defer is for the main site logic; async is for independent third-party scripts.
  • Both attributes are ignored on inline scripts that have no src.

Quick example

html
<!-- HTML is being parsed... --> <script src="main.js" defer></script> <script src="analytics.js" async></script>
StepWhat happens
1The browser starts parsing HTML
2It downloads both scripts at the same time
3analytics.js (async) downloads faster, so it runs immediately
4The HTML finishes parsing
5Then main.js (defer) runs
6Then DOMContentLoaded fires

The problem defer and async solve

When the browser meets this tag:

html
<script src="script.js"></script>

by default it:

  1. Stops parsing HTML;
  2. Downloads script.js;
  3. Executes it;
  4. And only then carries on building the DOM.

This blocks page rendering, so the user sees a blank screen until the JS has loaded. That is exactly why the defer and async attributes appeared: to avoid blocking HTML parsing.

How defer works

html
<script src="script.js" defer></script>
  1. The script downloads in parallel with the HTML, asynchronously.
  2. But it executes only after the HTML is fully parsed, that is when the DOM is ready, and before the DOMContentLoaded event.
  3. Scripts with defer execute in the order in which they appear in the HTML.

This is an excellent fit for the main application scripts, for example main.js or vendor.js.

html
<script src="a.js" defer></script> <script src="b.js" defer></script>

Both download in parallel first, but they execute strictly in sequence: a.js first, then b.js, and only once the DOM has been built. That makes defer safe for code that touches page elements straight away and for files that depend on one another.

How async works

html
<script src="script.js" async></script>
  1. The script downloads in parallel with the HTML, just as with defer.
  2. But it executes as soon as it has downloaded, even if the HTML is not fully parsed yet.
  3. Scripts with async execute in an unpredictable order, depending on which one downloads first.

It suits independent scripts, for example analytics or advertising.

html
<script src="a.js" async></script> <script src="b.js" async></script>

Here b.js may run before a.js if it downloaded faster, even though it sits lower in the HTML. On top of that, running an async script interrupts HTML parsing at the moment the file arrives, so a heavy async script can still slow rendering down.

Comparison

Propertydeferasync
Script downloadAsynchronous, in parallel with HTMLAsynchronous
ExecutionAfter the HTML is fully parsedImmediately after the download
Execution orderPreservedNot guaranteed
Blocks HTML parsingNoNot while downloading, but yes while executing
The DOMContentLoaded eventWaits for all defer scriptsDoes not wait
Typical scenariosThe main site logicExternal trackers, advertising

What to choose in practice

OptionWhen to use it
deferFor the main JS code that depends on the DOM, for example interface initialisation
asyncFor independent scripts: analytics, metrics, advertising
No attributeOnly when you genuinely need to block HTML parsing, which is rare

A tip: if you use a modern bundler (Next.js, Vite, Webpack), it adds defer to the <script> in head for you, so in modern applications this is already the default behaviour. It is also worth remembering that <script type="module"> behaves like defer automatically, and to get async behaviour for a module you have to state the attribute explicitly.

Common mistakes

  • Putting defer or async on an inline script. Without a src both attributes are ignored and the script runs synchronously in place.
  • Relying on order with async. If b.js needs variables from a.js, async breaks that randomly and only on a slow network.
  • Using async for code that works with the DOM. The script can run before the element it needs exists in the document, and querySelector returns null.
  • Confusing "does not block parsing" with "does not block at all". An async script still occupies the main thread while it executes.
  • Expecting DOMContentLoaded to wait for async. The event can fire before such a script, so a listener registered inside an async file sometimes simply arrives too late.
  • Leaving document.write() in a deferred script. After the document has been parsed it wipes the page, so it must not be used with async or defer.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.