Back to Performance
Performance
easy
mid

What are the different JavaScript script loading strategies (defer, async, modulepreload)?

`<script>` (default): parser-blocking. `async`: fetched in parallel, executed ASAP, may interleave with parsing. `defer`: fetched in parallel, executed after parse in order. `type="module"`: defer by default. `modulepreload`: warm the cache for module dependencies. Rule of thumb: `defer` app code, `async` independent (analytics), preload critical, modulepreload module graphs.

4 min read·~8 min to think through

The four behaviors

AttributeFetchExecuteOrder
(none)Sync, blocks parserImmediately, blocks parserSource order
asyncAsync, in parallelASAP after downloadWhichever finishes first
deferAsync, in parallelAfter parseSource order
type="module"Async, in parallelAfter parse (defer by default)Source order

Diagrams (mental)

ts
[Sync script]
HTML parse: ▮▮▮▮[stop]▮▮▮▮▮▮▮ → script DL+exec → continue

[async]
HTML parse: ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮
script fetch: ──────────exec→  (may interrupt parse during exec)

[defer]
HTML parse: ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ → DOMContentLoaded → exec in order
script fetch: ─────────

Rules of thumb

  • App code that touches the DOMdefer (or module).
  • Independent scripts (analytics, A/B test loaders, error reporters) → async.
  • Critical above-the-fold scripts<link rel="preload"> + use early.
  • Module graphs with many imports → <link rel="modulepreload" href="..."> for each.

Module preload

ES module imports cascade — a module's imports aren't discovered until it's fetched and parsed:

html
<script type="module" src="/app.js"></script>

app.js imports ./router.js, which imports ./pages.js — the browser learns about each only after the previous one loads. Waterfalls.

Mitigate with modulepreload:

html
<link rel="modulepreload" href="/router.js">
<link rel="modulepreload" href="/pages.js">
<script type="module" src="/app.js"></script>

Browser fetches all in parallel.

preload vs prefetch vs preconnect

HintUse
preloadResource needed for THIS navigation — fetch ASAP, high priority.
prefetchLikely needed for NEXT navigation — fetch idle priority.
preconnectOpen a connection (DNS + TCP + TLS) to an origin you'll hit soon.
dns-prefetchJust resolve DNS for an origin.
modulepreloadLike preload but for ES module dependencies (parses + populates module map).

Pitfalls

  • async on app code that depends on order: scripts may execute in any order.
  • Forgetting type="module" when using import statements → SyntaxError.
  • Module + sync anti-pattern (<script src="module.js"> without type=module).
  • Inline scripts ignore async/defer.

Order of execution on a typical page

  1. Parser-blocking sync scripts (avoid).
  2. CSS parses in parallel (render-blocking but not parser-blocking for non-CSS-querying scripts).
  3. async scripts execute as they arrive.
  4. defer + module scripts execute after parse in source order.
  5. DOMContentLoaded.
  6. Load event after subresources.

Interview framing

"Default <script> is parser-blocking — never put it in head without defer/async. defer: fetched in parallel, executed after parse in source order — right for app code. async: fetched in parallel, executed ASAP — right for independent scripts like analytics. type="module" is defer by default. ES modules cascade imports — use <link rel="modulepreload"> for each module in the critical graph to fetch them in parallel instead of a waterfall. <link rel="preload"> for non-module critical resources; prefetch for next-nav; preconnect for cross-origin handshakes."

Follow-up questions

  • When does async cause bugs?
  • How does modulepreload differ from preload?
  • Why is type=module defer by default?

Common mistakes

  • Sync script in head.
  • async on order-dependent app code.
  • Forgetting modulepreload → import waterfalls.

Performance considerations

  • defer/async cut TTI dramatically. modulepreload avoids module waterfall (multi-second wins on deep graphs).

Edge cases

  • Inline scripts ignore async/defer.
  • Dynamic <script> insertion semantics.
  • Importmaps + module resolution.

Real-world examples

  • Next.js Script component strategies, web.dev preload guides, Lighthouse render-blocking audits.

Senior engineer discussion

Seniors audit script tags in PR review and ensure modulepreload covers the critical module graph.

Related questions