Defer JavaScript: What It Means and How to Do It Right
Learn what it means to defer JavaScript, when to choose it over async, and how our free report checks the defer attribute on your scripts.
Free to use 16 min read Updated Oct 2026
Defer JavaScript in one minute
Quick answer: To defer JavaScript is to add the
deferattribute to an external script tag, so the browser downloads the file while it keeps reading the page and runs it only after the HTML is fully parsed. Our report wants a defer value on every external script it checks, and it reads that value literally.
- Pass: Every external script the report checks, up to the first 8 on the page, carries a defer value the report can read, which means
defer="defer". - Fail: Some scripts lack it (shown as a warning) or none have it (shown as failed), and both outcomes bring up the red How to Fix button.
- Fix: Add defer to each script the first paint does not need, and write it as
defer="defer"so the report counts it.
A rule of thumb that holds up well in practice: defer your own code and any library that has plugins depending on it, use async for independent scripts such as analytics, and leave tiny inline snippets alone.
One quirk before you start. If a script you already deferred still shows up as a problem, rewrite its tag with defer="defer" instead of a bare defer. The section on how our report tests this explains why.
What does it mean to defer JavaScript?
A normal script tag is an instruction to stop. When the HTML parser meets <script src="app.js">, it pauses, the browser fetches the file, runs it, and only then carries on building the page. One slow script high in the document can therefore leave a visitor staring at a blank screen. People call this a render blocking JavaScript file or a parser blocking script, and it sits squarely on the critical rendering path.
Adding defer changes the order of events. The download starts at once and runs in parallel with parsing, so the critical rendering path is no longer held up by the network. Execution is held back until the parser has finished the document, and it happens just before the DOMContentLoaded event, which itself waits for every deferred script to finish. When a page has several deferred scripts, they run in the order their tags appear in the markup.
A few boundaries are worth knowing. Defer only works on external scripts, meaning tags with a src attribute, and the browser ignores it on an inline script. The script defer attribute is also redundant on ECMAScript modules (type="module"), which are deferred by default; the nomodule attribute is a separate switch for old browsers and has nothing to do with it. Because defer is a boolean attribute, a bare defer, defer="" and defer="defer" all mean the same thing to a browser. The MDN script element reference documents each rule, and the HTML Living Standard from the WHATWG is the formal source behind it.
Defer delays running, not downloading. That separates it from the "delay JavaScript" options in some optimization plugins, which hold a file back until the first click, scroll or tap and carry their own risks. Lazy load, which postpones images, and preload, which fetches a file early, are different tools again. Older speed tools called this advice defer parsing of JavaScript, while Lighthouse and PageSpeed Insights now file the same problem under render blocking requests.
Defer vs async vs a normal script
Three loading modes sit side by side in the HTML specification, and the difference between them decides whether a page breaks when you add defer. The async vs defer question comes down to timing and order. The table compares them on the points that matter in daily work.
| Question | Normal script | async | defer |
|---|---|---|---|
| When does it download? | The parser stops while the file is fetched | In parallel with parsing | In parallel with parsing |
| When does it run? | Straight after the download, parser still paused | The moment the file arrives, pausing the parser | After parsing ends, just before DOMContentLoaded |
| Does it block the HTML parser? | During download and run | Only while it runs | Never |
| Is document order kept? | Yes | No | Yes |
| What can it safely touch? | Only the markup above its own tag | Unpredictable | The whole parsed document |
The practical choice comes down to dependency. Use defer for your own bundles and for a library whose plugins must load after it, because order is preserved and the document is complete when the code runs. Use the async attribute for independent third party scripts, such as an analytics script or a Google Tag Manager snippet, which do not care when they start. Google's guidance on loading third party JavaScript describes the same two behaviours: async runs as soon as the file arrives, while defer waits for the parsed document and keeps order.
If a single tag carries both async and defer, current browsers follow async. That pairing is a leftover from browsers that knew defer but not async, so skip it on a new page.
Our report is stricter than a browser here. An async script, or a module script with no defer attribute, is counted as not deferred even though a module script never holds the parser and an async script pauses it only while it runs. The next section shows exactly how the check works. For the wider audit of stylesheets and scripts that hold back the first paint, read the render blocking resources guide.
How SEO Site Checker tests defer JavaScript
The Defer Javascript row sits in the speed group of the SEO Site Checker report, next to JavaScript minification and JavaScript caching. All three rows read the same list of scripts, so a change to your script tags can move more than one row at once.
The rule: The report renders your page in headless Chromium, takes the first 8 external scripts in document order, downloads each one, and reads the defer value on its tag. Pass means every checked script has a defer value, and anything else shows the red How to Fix button.
- Pass: At least one script was checked and none of the checked scripts lacks a defer value.
- Fail: At least one checked script has no defer value, the page has no external scripts, or every download failed.
- Not tested: Inline scripts, any external script after the first 8, scripts still downloading when the 10 second window closes, whether the page still works once scripts are deferred, and how long the code takes to run.
What the report reads
Once headless Chromium has rendered the URL, the report collects every script element that has a src attribute, in the order they appear, and keeps the first 8. Inline scripts are ignored completely. Relative addresses such as /js/app.js are joined to the site root rather than to the folder of the page you tested, so a path written relative to a deep page can resolve to the wrong place.
Each file is then downloaded with short limits: 3 seconds per file and 10 seconds for the whole batch. Scripts still waiting when the 10 second window closes appear in neither list.
Pass, warning and fail
A warning means the page has a mix, some deferred and some not, and the message gives the number that lack it. A failed result covers three situations: no script has a defer value, the page has no external scripts at all, or every download failed. In the no scripts case the message reads 0 scripts without defer next to a failed icon. Warning and failed show the same red button, and only the icon differs.
Several things count as not deferred whatever the browser would do with them: async scripts, module scripts without a defer attribute, and any file the report could not download, including protocol relative addresses such as //cdn.example.com/a.js, which the downloader rejects. Signed-in users can open two collapsible lists, JavaScripts with defer and JavaScripts without defer, to see the actual URLs.
One more limit follows from this. The report reads the rendered page, so scripts that other scripts add later, such as those a tag manager or a chat widget injects, show up in its list, carry no defer value, and so count as not deferred. If one of the first 8 scripts is correctly async, a warning for it is a limit of the check, not a fault in your page, so do not remove async from an analytics tag just to turn a row green.
If a deferred script is still listed
The usual cause is the quirk from the top of this page: the report reads the defer value as text, and a bare defer comes through empty. The long form defer="defer" is valid HTML, behaves identically in every browser and is counted correctly. Edit the tag, clear your page and CDN caches, then run the report again.
If the script is still listed after that, look at the download. A firewall or bot protection rule may be refusing the request, the server may take longer than 3 seconds to answer, or the address may be protocol relative. Any of those puts the script in the not deferred list regardless of its tag.
Why deferring JavaScript helps page speed
A classic script placed in the head, or high in the body, makes the browser stop and wait, and nothing below it is painted until it finishes. That wait lands directly on First Contentful Paint and often on Largest Contentful Paint, because the headline or hero image sits behind the script. Defer removes the pause, since parsing and painting carry on while the file downloads.
A deferred script still runs before DOMContentLoaded, so the main thread does the same work, only later. Total Blocking Time and Interaction to Next Paint improve when you also ship smaller files, split code so a page loads only what it uses, and run less of it overall. That is why defer alone does not lower the figure in our JavaScript execution time guide.
As for rankings, Core Web Vitals are one signal among many, and relevance still outweighs them, so treat the gain as a better experience first. Our Core Web Vitals guide explains each metric and the thresholds behind it.
How to defer JavaScript in HTML
- List your external scripts. Sign in and open the two script lists in the report, or use View Source and search for script tags with a src. Note which ones the first paint truly needs, for example a script that builds the visible hero.
- Add defer to the rest. Put the attribute on every other external script, and write it as
defer="defer"so the report reads it. The first block below shows a normal tag, a deferred tag and an async tag side by side. - Keep dependent scripts in order. Place the library first and its plugins after it. Defer preserves document order and async does not.
- Fix inline code that calls a deferred library. An inline snippet runs immediately and finds nothing there yet. Move it into an external deferred file or wrap it in a
DOMContentLoadedlistener, as in the second block below. - Minify and compress first. Our free JavaScript minifier shrinks a script before you ship it.
- Test the page. Click through menus, forms, sliders and consent banners, and keep the browser console open to catch errors.
<!-- Normal: the parser waits for this file -->
<script src="/js/legacy.js"></script>
<!-- Deferred: downloads early, runs after parsing, order kept -->
<script src="/js/app.js" defer="defer"></script>
<!-- Async: independent analytics file, no order guarantee -->
<script src="/js/analytics.js" async></script>The deferred tag can sit in the head and still behave as if it were at the bottom, so you no longer have to move scripts to the footer. The next block covers inline code that depends on a deferred file.
document.addEventListener('DOMContentLoaded', function () {
// Deferred scripts have already run by this point
$('.menu').superfish();
});How to defer JavaScript in WordPress, Shopify and other platforms
WordPress 6.3 and later
Since version 6.3, WordPress accepts a loading strategy when you register or enqueue a script. Pass an args array as the fifth argument of wp_enqueue_script or wp_register_script, with the strategy parameter set to defer and, if you want the footer, in_footer set to true. Put the code in a child theme's functions.php or in a small plugin of your own. WordPress then checks the dependency tree, and it quietly falls back to a normal blocking script when any script that depends on it is blocking, or when inline code is attached after it with wp_add_inline_script (the default after position).
There is a catch for this report. Recent core code prints a bare defer attribute together with a data-wp-strategy attribute, and our check counts a bare defer as not deferred. If the row stays red after you use the strategy argument, the filter in the second part of the block below rewrites that attribute as defer="defer" for one handle. The guard at the top makes the filter change only the spelling of a defer that WordPress already applied, so a script that core downgraded to blocking stays blocking.
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script(
'theme-main',
get_template_directory_uri() . '/js/main.js',
array(),
'1.0.0',
array('strategy' => 'defer', 'in_footer' => true)
);
});
// Use only if the report row stays red
add_filter('script_loader_tag', function ($tag, $handle) {
if ($handle !== 'theme-main' || !preg_match('/\sdefer[\s=>]/', $tag)) {
return $tag;
}
return preg_replace('/\sdefer(=("|\')[^"\']*("|\'))?(?=[\s>])/', ' defer="defer"', $tag, 1);
}, 10, 2);The WordPress 6.3 script strategy announcement lists the exact rules for when a strategy is honoured. Confirm the printed tag in View Source after any change.
Older WordPress and plugins
On versions before 6.3 there is no strategy argument, so a small filter of this kind has to add the attribute by hand for the handles you list. The earlier filter will not do that job, because it only rewrites a defer that WordPress already printed. Adjust the handle to match your own script, and remove the filter once you move to 6.3, otherwise a tag can end up with the attribute twice.
add_filter('script_loader_tag', function ($tag, $handle) {
if ($handle === 'theme-main') {
return preg_replace('/ src=/', ' defer="defer" src=', $tag, 1);
}
return $tag;
}, 10, 2);Leave the jQuery core handle alone unless you have checked every inline script that calls it, because many themes print inline code that expects jQuery at once. To find out which theme owns the files you are editing, the WordPress Theme Detector can name it.
The other route is a free optimization plugin. LiteSpeed Cache and Autoptimize both offer a defer JavaScript option. Switch on one setting at a time, add exclusions for scripts that break, and test, then check the printed tags, because a plugin may not write the attribute the way our report reads it.
Shopify and hosted builders
In a Shopify theme, add defer to the script tags you control in theme.liquid and the other theme files, for example <script src="{{ 'theme.js' | asset_url }}" defer="defer"></script>. Scripts injected by installed apps sit outside the theme code, so those need the app's own setting or a conversation with the developer.
Wix, Squarespace and Webflow are different, because hosted builders write their own platform scripts and you cannot edit those tags. Where a custom code area accepts a full script tag, put defer on the tag you add there.
Mistakes to avoid when you defer JavaScript
Most failures after deferring come from a handful of causes. The first is adding defer to an inline script, where it does nothing, so the code still runs at once. The second is deferring a library but leaving an inline snippet that calls it immediately.
Common error: The console shows "Uncaught ReferenceError: jQuery is not defined" or "$ is not defined" right after you defer jQuery. An inline script is running before the deferred library has loaded. Move that code into a deferred file or inside a
DOMContentLoadedlistener.
Putting async on scripts that depend on each other is the third mistake, because a plugin can then run before its library. The fourth is deferring a tiny script that must run before the first paint, such as a theme switch or an A/B test snippet, which causes a visible flash. Keep those inline and small. A fifth cause is old code that calls document.write, which is common in legacy ad and widget snippets. After parsing has ended, that call can wipe the whole page, so such a script must stay a normal one or be replaced.
Warning: Do not defer everything in one go without testing. Checkout steps, contact forms and cookie consent tools are the usual casualties.
Finally, do not expect defer to clear every speed warning. A stylesheet in the head still blocks rendering, and a large hero image can keep Largest Contentful Paint red even after every script is deferred.
How to check that your scripts are deferred
- Start with our report. Open the JavaScripts without defer list and see which URLs remain.
- Read the source. In View Source, look for external script tags that have no defer, no async and no
type="module". Those are the ones that block the parser. View Source will not show scripts that JavaScript injects later, but the report will, so the two lists can differ. - Use the console. Paste the line below into the browser console. It prints external scripts that have neither the defer nor the async property and are not modules. It finds parser blocking scripts, not exactly the report list.
- Open the Network panel. In Chrome DevTools, deferred files should start downloading early in the waterfall, yet run after the HTML has been parsed.
- Purge caches if nothing changed. Clear page, plugin and CDN caches, and read the JavaScript caching guide if old script files linger.
console.log([...document.scripts].filter(s => s.src && !s.defer && !s.async && s.type !== 'module').map(s => s.src));How to Fix
The short version, the same steps you see when you click How to Fix in your report.
Why this fails: The report found external scripts without a defer attribute, so the browser stops reading the page to download and run them. A warning means some scripts are deferred, and a failed result means none are. The check reads the first 8 external scripts. Async scripts, module scripts and files it could not download count as not deferred, and so does a bare defer, so write defer="defer".
How to fix it:
- Sign in and open the two script lists in the report to see which script URLs lack defer.
- Add defer="defer" to each external script that the first paint does not need.
- Keep a library before the scripts that use it, because defer keeps document order.
- Move inline code that calls a deferred library into a deferred file or a DOMContentLoaded listener.
- Leave tiny scripts that must run before paint inline.
<script src="/js/app.js" defer="defer"></script>
<script src="/js/analytics.js" async></script>- WordPress 6.3 and later: pass a strategy of defer in the args array of wp_enqueue_script. If the row stays red, use the filter in the code block below.
- Older WordPress: use a script_loader_tag filter that adds defer="defer" to the handles you list (the filter below only respells a defer WordPress already printed), or the defer option of a free optimization plugin.
- Shopify: put defer="defer" on script tags in theme.liquid.
- Wix, Squarespace and Webflow: only on script tags you add in their custom code areas.
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script('theme-main', get_template_directory_uri() . '/js/main.js', array(), '1.0.0', array('strategy' => 'defer', 'in_footer' => true));
});
add_filter('script_loader_tag', function ($tag, $handle) {
if ($handle !== 'theme-main' || !preg_match('/\sdefer[\s=>]/', $tag)) {
return $tag;
}
return preg_replace('/\sdefer(=("|\')[^"\']*("|\'))?(?=[\s>])/', ' defer="defer"', $tag, 1);
}, 10, 2);Re-test: Purge page and CDN caches, then run our free report again on the same URL and confirm the Defer Javascript row is green. Click through menus, forms and sliders to make sure nothing broke. See the MDN script element reference for the attribute rules.
Frequently Asked Questions
What does defer mean in JavaScript?
What is the difference between async and defer in JavaScript?
How do I defer JavaScript in WordPress?
Can you defer inline JavaScript?
Can I defer jQuery?
How do I fix the defer parsing of JavaScript warning?
How do I check if deferring JavaScript improved performance?
See how your site scores right now
Paste your address into the free report, then use the How to Fix button on anything that needs work.
Free to use. Results in under a minute for most sites.