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.
In this guide
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.
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 downloads | When it runs | Order kept | DOMContentLoaded waits for it | |
|---|---|---|---|---|
| Plain | Yes | Right away | Yes | Yes |
async | No | When downloaded | No | No |
defer | No | After the page is read | Yes | Yes |
type="module" | No | After the page is read | Yes | Yes |
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
- Your own scripts that need the page's elements:
defer. - Your own scripts that depend on each other:
defer, in order. - Independent third-party tags such as analytics:
async. - 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.
- 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
- Scripts at the end of the body instead of `defer` in the head. The old habit works, but the browser finds
deferscripts earlier while it reads the head and starts the download sooner. - `async` on scripts that depend on each other. They run in whatever order they arrive, and the page breaks at random.
- `defer` on an inline script, which does nothing.
- A script that expects the page to be ready, loaded with `async`. It may run too early.
- Too many deferred scripts.
deferfixes blocking, not cost. Ten large scripts still use the main thread when they run, which is what Total Blocking Time measures. - Plugins that reorder scripts. On WordPress and similar systems, an optimization plugin may add
deferorasyncto everything. Test every feature after you turn it on.
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
- The script element on MDN:
async,defer, module scripts,nomoduleand what blocks parsing. - DOMContentLoaded on MDN: which scripts the event waits for, and the
readyStatecheck. - fetchpriority on MDN: raising or lowering a download's priority.
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