defer, async and module scripts: how scripts load

How the browser loads scripts by default, what async, defer and type module change, which events wait for which, and how to choose for each script.

IntermediateExplainer5 min readUpdated October 30, 2026

In this guide
  1. What happens by default
  2. async: run it as soon as it arrives
  3. defer: run it after the page has been read
  4. type="module": defer by default
  5. Which one waits for what
  6. Events and timing
  7. How to choose
  8. Common mistakes
  9. Priority and preloading
  10. Check your own page
  11. Sources

What happens by default

A browser reads HTML from top to bottom and builds the page as it goes. When it meets a script tag with a source address, it has to stop. The script might change the page it is reading, so the browser downloads the file, runs it, and only then carries on. A script in the head with no special attribute therefore holds up everything after it, including the content the visitor is waiting to see. That is why scripts are one of the two main kinds of render-blocking file, next to stylesheets. See render-blocking resources.

MDN draws a precise line: scripts without async, defer or type="module", and inline scripts without type="module", are fetched and executed immediately before the browser continues to parse the page. Strictly they block parsing, not rendering, but with a script in the head, nothing below it is parsed, so nothing below it appears.

Three ways a script can load, as timelines. A plain script stops the page from being read while it downloads and runs. An async script downloads while the page is read and pauses reading only while it runs. A defer script downloads while the page is read and runs after the page has been read. Plain script page script async page script defer page script reading the page downloading running the script Time runs left to right. Gaps in the page row are where reading stops.
The three ways a script loads. Only the plain script stops the page from being read while it downloads.

async: run it as soon as it arrives

An async script downloads in parallel with the reading of the page and runs the moment the download completes. The page keeps being read during the download, but it pauses while the script runs, so a long script still interrupts it.

<script src="https://analytics.example.com/tag.js" async></script>

Two properties matter. First, there is no guarantee of order: if two async scripts are on the page, the one that finishes downloading first runs first. Second, there is no guarantee about timing relative to the page: an async script may run before the page has been read, or long after. Use async for scripts that depend on nothing else on the page and that nothing else depends on, such as analytics.

defer: run it after the page has been read

A defer script also downloads while the page is read, but it waits to run until the whole document has been parsed. Deferred scripts run in the order they appear, and they run before the DOMContentLoaded event fires. This makes defer the right choice for scripts that touch the page's elements or depend on each other.

<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" defer></script>

Here app.js always runs after vendor.js, even if it downloads first. A deferred script does not wait for the images, but it does wait for stylesheets that were already in the page, because it may read styles.

type="module": defer by default

A script with type="module" is a JavaScript module. It is deferred automatically, so the defer attribute adds nothing, and it can use import to load its own dependencies. Adding async to a module makes it run as soon as it and its imports have downloaded, as with a classic async script.

<script type="module" src="/js/app.mjs"></script>
<script nomodule src="/js/legacy-bundle.js" defer></script>

Browsers that understand modules ignore a script marked nomodule, so the second line is a fallback for very old browsers. Most sites no longer need one.

Which one waits for what

Blocks reading the page while it downloadsWhen it runsOrder keptDOMContentLoaded waits for it
PlainYesRight awayYesYes
asyncNoWhen downloadedNoNo
deferNoAfter the page is readYesYes
type="module"NoAfter the page is readYesYes

async and defer only work on scripts with a src. On an inline script they have no effect, so a script written directly in the page always runs when the browser reaches it, unless it is a module.

Events and timing

DOMContentLoaded fires when the HTML has been parsed and every deferred and module script has downloaded and run. It does not wait for images or for async scripts. The load event comes later, after the images and other resources have arrived.

This matters if your code waits for the page. A script that runs asynchronously, or is added later by another script, may start after DOMContentLoaded has already fired, so a listener added then never runs. MDN's advice is to check document.readyState first:

function start() { /* set up the page */ }

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', start);
} else {
  start();
}

A defer script never needs this check: it always runs while readyState is interactive, before the event.

How to choose

  1. Your own scripts that need the page's elements: defer.
  2. Your own scripts that depend on each other: defer, in order.
  3. Independent third-party tags such as analytics: async.
  4. A script that must run before the page is drawn, for example one that chooses a theme to avoid a flash: leave it plain, put it inline or as small as possible, and keep it very short.
  5. Anything not needed until later: load it when it is needed, as in third-party scripts. For your own code, a dynamic import does the same job; see bundle size and code splitting.

Common mistakes

Priority and preloading

Scripts also have a download priority, and you can raise or lower it with fetchpriority on the script tag, and start the download early with a preload or modulepreload hint. Use them for one or two critical scripts, not for everything. The critical CSS guide covers the related ideas for stylesheets, and MDN's page on the fetchpriority attribute warns that excessive prioritization can make a page slower.

Check your own page

Run the free audit and look at the render-blocking requests. For each script on the list, decide whether it needs to run before the page is drawn. Most do not: add defer to your own, async to independent third-party tags, test the page, and run the audit again.

Sources

Start here

Free website audit

Live Google data for any public page: mobile and desktop.

Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it

Rather have this done for you? Speed optimization · Book a call

Related guides

All guides →