# async vs defer: loading JavaScript without blocking the page

> async and defer both download a script without blocking the page, but defer runs scripts in order after the HTML is parsed, while async runs each one as soon as it arrives. When to use which, how modules and inline scripts behave, and how to defer JavaScript in WordPress.

Updated 2026-09-26 · Speed & Core Web Vitals · HTML version: https://getreport.app/guides/async-vs-defer

`async` and `defer` both tell the browser to download a script without pausing the HTML parser, so neither blocks the page from rendering. The difference is when the script runs: `defer` waits until the HTML is fully parsed and runs scripts in the order they appear, while `async` runs each script the moment it finishes downloading, in whatever order they arrive. Use `defer` for your own scripts and anything that depends on another script; use `async` for independent scripts such as analytics. This guide explains the timing, the edge cases and how to apply it on a real site. It is part of our [guide to eliminating render-blocking resources](https://getreport.app/guides/render-blocking-resources).

## Quick answer

- **No attribute:** the parser stops, the script downloads and runs, then parsing continues. It blocks rendering.
- **`defer`:** downloads in parallel, runs after the HTML is parsed, in document order, before `DOMContentLoaded`.
- **`async`:** downloads in parallel, runs as soon as it arrives, possibly mid-parse, in no guaranteed order.
- **Use `defer`** for your own scripts, libraries and plugins that depend on each other (jQuery and its plugins).
- **Use `async`** for scripts that depend on nothing and that nothing depends on: analytics, tag managers, some ad and widget loaders.
- **Modules** (`type="module"`) are deferred by default. **Inline scripts** ignore both attributes.

## What happens without async or defer

A plain `<script src="…">` in the `<head>` is parser-blocking. When the browser reaches it, it stops reading the HTML, downloads the file, runs it, and only then continues. Nothing below the tag can be rendered until that is done. With three such scripts in the head, the visitor waits for three downloads and three executions before seeing anything, which shows up as a later First Contentful Paint and Largest Contentful Paint.

The browser's preload scanner does look ahead and start downloading later files early, which softens the cost, but it cannot run them early. The order and the pause stay.

## How defer works

```html
<script src="https://getreport.app/js/jquery.min.js" defer></script>
<script src="https://getreport.app/js/slider.js" defer></script>
```

1. The parser sees the tag, starts the download in the background and keeps parsing.
2. When the whole document has been parsed, the browser runs the deferred scripts **in the order they appear in the HTML**, regardless of which download finished first.
3. Then the `DOMContentLoaded` event fires. It waits for deferred scripts.

Because deferred scripts run after parsing, the whole DOM exists when they run, so they can find any element on the page. Because they keep their order, `slider.js` can safely use jQuery loaded by the script before it.

`defer` only works on external scripts, those with a `src` attribute. On an inline script it is ignored.

## How async works

```html
<script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" async></script>
```

1. The parser sees the tag, starts the download and keeps parsing.
2. As soon as the file arrives, the parser pauses briefly and the script runs, whether the HTML is finished or not.
3. Several async scripts run in the order their downloads finish, which you cannot predict.

`DOMContentLoaded` does not wait for async scripts. An async script may run before the parser has reached the element it needs, so it should not assume the page is complete.

## async vs defer side by side

| | No attribute | `defer` | `async` |
| --- | --- | --- | --- |
| Blocks parsing while downloading | Yes | No | No |
| When it runs | Immediately | After parsing, before `DOMContentLoaded` | As soon as it arrives |
| Can interrupt parsing | Yes | No | Yes, briefly |
| Order between scripts | Document order | Document order | Download order |
| DOM available when it runs | Only what is above it | All of it | Unknown |
| Works on inline scripts | n/a | No | No (except modules) |
| Best for | Scripts the first paint truly needs | Your own code, dependencies, libraries | Independent third-party tags |

If a script has both attributes, `async` wins in every current browser. Old pages sometimes use `async defer` together, a pattern from the time when some browsers supported only `defer`; it behaves as `async` today.

## Which one should I use?

Ask two questions about each script:

1. **Does it depend on another script, or does another script depend on it?** Then use `defer` on all of them, so their order is kept. jQuery, its plugins and your theme script are the classic case.
2. **Does it need the whole page to exist?** Then `defer`, which guarantees the DOM is parsed.

If the answer to both is no, `async` is fine and runs the script a little sooner. Analytics and tag managers are designed for it; their own snippets usually include `async`.

A few scripts should stay blocking or be rethought:

- **A/B testing and personalisation scripts** that change the first screen. Deferring them causes a visible flicker as content changes after it appears. Keep them small and early, or do the work on the server.
- **Scripts that render the first screen,** such as a JavaScript-built hero. Better to send that content as HTML.
- **Scripts that use `document.write()`.** Browsers ignore `document.write()` calls from async and deferred scripts, so the content they write never appears. Replace them; Chrome also blocks `document.write()` of scripts in some conditions.

> **Check: document.write() is not used.** document.write blocks the page until the injected script arrives, and Chrome drops such scripts on slow connections, so the ad or widget simply does not appear.
>
> 1. Replace document.write with a normal <script async> tag or by appending the element from JavaScript.
> 2. Update the ad or widget snippet; most vendors now provide an async version.

## Modules, inline and injected scripts

**Modules.** `<script type="module" src="app.js">` is deferred by default: it downloads in parallel and runs after parsing, in order. Adding `async` to a module makes it run as soon as it and its imports are ready. Inline modules can be `async` too, which is the one case where an attribute affects an inline script.

**Inline scripts.** `<script>…</script>` without `src` runs immediately, and `defer` and `async` are ignored. That creates a common bug: an inline snippet that calls `jQuery(…)` fails after you defer jQuery, because jQuery has not run yet. Fix it by moving the snippet into a file loaded with `defer` after the library, or by wrapping it:

```html
<script>
  document.addEventListener('DOMContentLoaded', function () {
    jQuery('.slider').slick(); // runs after all deferred scripts
  });
</script>
```

**Injected scripts.** A script added with JavaScript (`document.createElement('script')`) is async by default. To keep several injected scripts in order, set `script.async = false` on each; they still do not block parsing.

## What about putting scripts at the end of the body?

Moving scripts just before `</body>` was the standard advice before `defer` was widely supported, and it still works: everything above the scripts is parsed and painted first. `defer` in the `<head>` is usually better, because the browser discovers and starts downloading the file much earlier, while the rest of the HTML is still arriving, and the order and timing are the same.

## What "defer parsing of JavaScript" means

"Defer parsing of JavaScript" is an older speed-test recommendation that means the same thing in practice: stop scripts from blocking the first render by loading them with `defer` or `async`, or later. Many WordPress plugins still use the phrase for their "defer JavaScript" setting. It is not about the browser's JavaScript parser itself; the script is still parsed and compiled when it runs.

Some plugins go one step further with "delay JavaScript execution", which loads chosen scripts only after the first user interaction (a scroll, tap or key press). That can help a lot with heavy third-party tags, but it breaks anything that must work without interaction, such as a cookie banner or above-the-fold slider, so test each script you delay.

## How to defer JavaScript in WordPress

Since WordPress 6.3, `wp_enqueue_script()` and `wp_register_script()` accept a loading strategy, so you do not need to filter the HTML:

```php
// functions.php of a child theme, or a small plugin
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'theme-main',
        get_stylesheet_directory_uri() . '/js/main.js',
        ['jquery'],
        '1.0',
        ['strategy' => 'defer', 'in_footer' => false]
    );
});
```

WordPress checks the dependency tree: if a script that depends on yours is still blocking, WordPress quietly loads yours blocking too, so the order stays safe. That is why adding `defer` to one script sometimes has no visible effect: its dependants need a strategy as well.

For scripts added by plugins you did not write, use a performance plugin's "defer JavaScript" option, or an asset manager to stop them loading on pages that do not use them. The plugin-by-plugin walkthrough is in [eliminating render-blocking resources and unused CSS on WordPress](https://getreport.app/guides/render-blocking-and-unused-css-wordpress).

## How to check the result

> **Free tool:** [Find and remove unused CSS and JavaScript](https://getreport.app/tools/unused-code): Free tool to remove unused CSS and JavaScript: real Chromium coverage shows the waste in every file, third parties marked, with Google's estimated time saved.

The unused code checker loads the page in Chromium with code coverage on and lists every script by unused bytes, first-party and third-party apart, with Google's estimate of the time you would save. Deferring fixes when a script runs; this shows which scripts should not load on that page at all. The speed test's render-blocking finding lists any script still blocking the first paint:

> **Check: No render-blocking resources delay the first paint.** Stylesheets and scripts in <head> stop the browser from drawing anything until they have downloaded. Each one adds a round trip before the visitor sees the page.
>
> 1. Add defer or async to scripts that are not needed before the first paint, or move them to the end of <body>.
> 2. Inline the CSS needed for the first screen and load the rest asynchronously (WordPress: "Optimize CSS delivery" in WP Rocket / LiteSpeed Cache).
> 3. Remove unused plugin CSS/JS from pages that do not need it.

> **Check: Little unused JavaScript is downloaded.** JavaScript is the most expensive thing a page loads: it must be downloaded, parsed and run. Code that never executes still costs all of that on a phone.
>
> 1. Delay third-party scripts until interaction (WP Rocket "Delay JavaScript", Shopify: audit installed apps).
> 2. Code-split large bundles so each page loads only its own code.
> 3. Remove plugins, apps and tags you no longer use.

After the change, click through the pages that use the scripts: menus, sliders, forms, the cart and checkout. Open the browser console and look for errors such as "jQuery is not defined", which means an inline script now runs before its library. For heavy third-party tags that still cost time after deferring, see [third-party scripts: tag managers, chat and ads](https://getreport.app/guides/third-party-scripts-tag-managers-chat-and-ads).

## Questions people ask

### Is async or defer better for performance?

Both remove the blocking download, so both improve the first paint. `defer` is the safer default because it keeps script order and runs after the page is parsed, so nothing breaks. `async` can run a script slightly sooner, which suits independent tags such as analytics, but it can interrupt parsing and scripts run in unpredictable order.

### Does defer work on inline scripts?

No. The `defer` attribute only applies to scripts with a `src` attribute; on an inline script it is ignored and the code runs immediately. To run inline code after deferred scripts, wrap it in a `DOMContentLoaded` event listener or move it into a small external file loaded with `defer` after the library it uses.

### Should analytics scripts use async or defer?

Usually `async`, which is what Google Analytics and Google Tag Manager snippets use. Analytics does not depend on your other scripts, so running it as soon as it arrives is safe, and it starts measuring slightly earlier. If you care more about the first paint than about counting very short visits, `defer` is also fine.

### Does DOMContentLoaded wait for async scripts?

No. `DOMContentLoaded` fires after the HTML is parsed and all deferred scripts have run, but it does not wait for async scripts. An async script may run before or after the event, depending on when its download finishes. Code that must run after the page is ready should use `defer` or listen for the event.
