Skip to main content

Difference between script, async and defer

script, async, and defer control when the browser fetches and runs JavaScript relative to HTML parsing.

Theory

TL;DR

  • Plain <script> stops the HTML parser, fetches the file, executes it, then continues. Blocking.
  • async fetches in parallel and runs the script as soon as the download finishes. No order guarantee.
  • defer fetches in parallel but waits until the full HTML is parsed, then runs scripts in declared order.
  • Analogy: the HTML parser is a chef reading a recipe. Plain <script> makes them stop chopping and handle the task right now. async hands it to an assistant who can interrupt at any moment. defer hands it off and the assistant steps in only after the chef reads the whole recipe, in sequence.
  • Decision rule: defer for app scripts, async for independent third-party tools like analytics.

Quick example

html
<!DOCTYPE html> <html> <head> <!-- Blocks parser until fetched + executed --> <script src="slow.js"></script> <!-- Fetches in parallel, runs whenever ready (no order guarantee) --> <script src="analytics.js" async></script> <!-- Fetches in parallel, runs after full parse, in declared order --> <script src="app.js" defer></script> </head> <body> <div id="root"></div> </body> </html>

If slow.js takes 2 seconds to download, async and defer don't hold up the parser. Plain script does.

Key difference

The real split is when execution happens. Both async and defer use the browser's preload scanner to fetch files without blocking the parser. But async runs the script the moment its download finishes, which could be mid-parse. At that point the DOM is incomplete. defer puts the script into an ordered callback list and drains that list only after the parser finishes, before DOMContentLoaded fires.

When to use

  • Main app bundle (React, Vue, vanilla JS) - use defer. DOM is ready, order is preserved.
  • Analytics and ads (Google Analytics, Facebook Pixel) - use async. They don't depend on page elements.
  • Multiple scripts with dependencies (jQuery + plugin) - use defer on both. Declared order is respected.
  • Small inline critical code - plain <script> in <head>. No fetch needed, fast enough.
  • Plain script at body bottom - acceptable for small pages, but defer in <head> is cleaner.

Comparison table

Aspect<script>asyncdefer
Blocks HTML parsingYes (fetch + exec)No (fetch), briefly (exec)No
FetchSynchronousParallelParallel
Execution timingImmediately on encounterWhen download finishesAfter full HTML parse
Order guaranteeN/ANoYes
DOM accessOnly above the tagUnreliable (may be mid-parse)Full
Best forSmall inline, end-of-bodyIndependent (analytics, ads)App scripts, libraries

How the browser handles this

When the parser hits a plain <script>, it stops, hands the URL to the network layer, waits, then runs the script in the main thread. async and defer both get picked up by the preload scanner, a separate speculative pass that reads ahead in the HTML stream without building the DOM. The difference is the queue. async scripts execute off the async queue when their download finishes. defer scripts go into a per-document deferred list and run after document.readyState becomes "interactive", before DOMContentLoaded.

This one trips people up regularly: defer is silently ignored on inline scripts without a src attribute. The HTML spec says so. Inline code with defer runs immediately, same as plain <script>.

Common mistakes

Using async for scripts that depend on each other

html
<!-- Wrong: initMap() may run before maps.js is ready --> <script async src="maps.js"></script> <script async src="app.js"></script> <!-- app.js calls initMap() --> <!-- Fix: defer preserves declared order --> <script defer src="maps.js"></script> <script defer src="app.js"></script>

async turns each script into a race. If app.js downloads first, initMap is not defined yet. No error, just silence.

Adding defer to an inline script

html
<!-- Wrong: defer is ignored here, runs immediately --> <script defer> document.getElementById('root').innerHTML = 'Hello'; </script> <!-- Fix: move to external file or wrap in DOMContentLoaded --> <script> document.addEventListener('DOMContentLoaded', () => { document.getElementById('root').innerHTML = 'Hello'; }); </script>

Combining async with a DOMContentLoaded listener inside the script

html
<script async src="app.js"></script> <!-- app.js: document.addEventListener('DOMContentLoaded', init) -->

If app.js downloads after DOMContentLoaded already fired, init never runs. No error is thrown. Use defer instead. Deferred scripts run before DOMContentLoaded, so the listener always registers in time.

Blocking <script> in <head> for performance reasons

A render-blocking script in <head> delays First Contentful Paint. Even a 100ms network request is enough to push FCP noticeably in Lighthouse. Move it to body bottom or add defer.

Real-world usage

  • React (Vite / CRA output) - defer on the bundle; DOM is ready when ReactDOM.createRoot runs.
  • Google Analytics (gtag.js) - async in <head>; tracking pixel with no DOM dependency.
  • jQuery + Bootstrap plugins - defer on both; jQuery loads first, plugin finds it.
  • Facebook Pixel - async; fires events independently.
  • Webpack's html-webpack-plugin - injects defer by default on output chunks, which is why most React apps just work without developers thinking about this.

Follow-up questions

Q: What happens if you add both async and defer to the same script?
A: async wins. Browsers treat it as async and ignore defer when both are present.

Q: Does defer guarantee execution before DOMContentLoaded?
A: Yes. Deferred scripts run after full HTML parse and before DOMContentLoaded fires. That is the contract the spec defines.

Q: Can inline scripts use defer?
A: No. defer has no effect on scripts without a src. They execute immediately regardless of the attribute.

Q: How does the preload scanner detect async and defer before the parser reaches those tags?
A: The preload scanner is a secondary tokenizer that reads the HTML stream without building the DOM. It finds src attributes on <script async> and <script defer> early and queues the fetches at lower priority, so the download is already in flight by the time the main parser gets there.

Q: How do type="module" scripts behave compared to defer?
A: Module scripts behave like defer by default. They fetch in parallel and execute after parse, in order. Adding async to a module script makes it run as soon as it is ready, same as a regular async script.

Examples

Blocking script delays page render

html
<!DOCTYPE html> <html> <head> <title>Blocking example</title> <!-- Fetches over the network before any body content renders --> <script src="heavy-analytics.js"></script> </head> <body> <h1>Users see this later than they should</h1> </body> </html>

The <h1> is not painted until heavy-analytics.js downloads and runs. Moving it to async or placing it at body bottom removes the block entirely.

React app with deferred scripts

html
<!DOCTYPE html> <html> <head> <title>App</title> <!-- All three fetch in parallel, but execute in declared order --> <script defer src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script defer src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <script defer src="bundle.js"></script> </head> <body> <div id="root"></div> <!-- By the time bundle.js runs, #root exists and React is loaded --> </body> </html>

defer preserves the order: React, then ReactDOM, then the app bundle. The #root div is in the DOM when ReactDOM.createRoot runs. No setTimeout hacks or readyState checks needed.

async mid-parse race condition

html
<!DOCTYPE html> <html> <head> <!-- async runs when ready, potentially before <body> is parsed --> <script async src="tracker.js"></script> </head> <body> <form id="signup">...</form> <!-- May not exist when tracker.js executes --> </body> </html>

If tracker.js calls document.getElementById('signup'), it may return null. The form has not been parsed yet. defer removes this uncertainty: by execution time, the full DOM is available.

Short Answer

Interview ready
Premium

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

Finished reading?