Every WordPress speed audit eventually lands on the same PageSpeed Insights warning: “Eliminate render-blocking resources”. Nine times out of ten, the culprit is a pile of <script> tags loaded in the <head> that the browser must download and execute before it can paint a single pixel.
The fix is simple in theory: defer JavaScript. The problem is that most tutorials hand you a copy-paste snippet that defers everything, including jQuery, and your sliders, menus and contact forms quietly stop working.
This guide takes the opposite approach. You will learn how to defer JavaScript in WordPress manually and selectively, using the native script_loader_tag filter, with a protected exclusion list and a real testing checklist you can run in under 10 minutes.
Defer vs Async: What Actually Happens in the Browser
Before you touch any code, you need to know which attribute to apply. They are not interchangeable.
| Attribute | Download | Execution | Order preserved? | Best for |
|---|---|---|---|---|
| (none) | Blocks HTML parsing | Immediately | Yes | Critical inline-dependent scripts |
| defer | Parallel, non-blocking | After HTML is parsed, before DOMContentLoaded | Yes | Theme scripts, plugin scripts, anything with dependencies |
| async | Parallel, non-blocking | As soon as it finishes downloading | No | Standalone scripts (analytics, chat widgets, pixels) |
Rule of thumb: if a script depends on another script, use defer. If it is completely self-contained, async is fine. When in doubt, use defer, because it keeps execution order intact and that is what prevents 90% of breakages.

Before You Start: 4 Non-Negotiables
- Work on a staging site first, or at minimum take a full backup (files + database).
- Use a child theme or a site-specific plugin. Never edit a parent theme’s
functions.php, it gets wiped on update. A small custom plugin or a snippet manager is cleaner. - Disable your caching and optimization plugin’s own “defer JS” option while you test. Two systems deferring the same scripts is a classic source of double-attribute bugs.
- Record a baseline: run PageSpeed Insights on your homepage and one inner page, and save the LCP, TBT and “render-blocking resources” values. You cannot prove an improvement without a before.
Step 1: Find the Script Handles You Want to Defer
WordPress registers every script with a unique handle (for example jquery-core, contact-form-7, swiper). You need those handles to target scripts precisely.
Option A: The debug snippet (fastest)
Add this temporarily. It prints an HTML comment listing every handle loaded on the page, visible only to administrators.
add_action( 'wp_print_footer_scripts', 'ppp_list_script_handles', 9999 );
function ppp_list_script_handles() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wp_scripts;
echo "\n<!-- SCRIPT HANDLES: " . implode( ', ', (array) $wp_scripts->done ) . " -->\n";
}
Load your homepage while logged in, press Ctrl+U (view source), and search for SCRIPT HANDLES. Copy the list somewhere, then remove the snippet when you are done.
Option B: Read the source directly
WordPress appends the handle as an id attribute on every enqueued script tag:
<script src="https://example.com/wp-content/plugins/slider/slider.js" id="swiper-js"></script>
The handle is swiper (WordPress adds the -js suffix to the id). Simple and reliable.

Step 2: The Safe Exclusion List
This is the part every “defer javascript wordpress” snippet on the internet skips. These handles should normally never be deferred on a standard site:
jquery,jquery-core,jquery-migrate– because dozens of plugins print inlinejQuery(...)calls that run before a deferred jQuery would ever load. Result: “$ is not defined”.wp-i18n,wp-hooks,wp-polyfill– core dependencies used by blocks and many plugin scripts.- Anything that powers above-the-fold rendering: cookie consent banners, sticky header logic, hero sliders, A/B testing scripts.
- Payment and checkout scripts: Stripe, PayPal, WooCommerce checkout,
wc-checkout,wc-cart. google-recaptchaand similar anti-spam scripts that hook into form submission timing.- Anything registered by your page builder’s frontend editor.
Keep this list in a variable so you can extend it as you discover edge cases.
Step 3: The script_loader_tag Snippet (Deny List Approach)
This version defers everything except what is on your exclusion list. It is aggressive, so it gives the biggest performance win but requires the most testing.
/**
* Defer JavaScript in WordPress, with a safe exclusion list.
*/
add_filter( 'script_loader_tag', 'ppp_defer_frontend_scripts', 10, 3 );
function ppp_defer_frontend_scripts( $tag, $handle, $src ) {
// Never touch admin, login, customizer or feed output.
if ( is_admin() || is_customize_preview() || is_feed() ) {
return $tag;
}
// Handles that must keep loading normally.
$excluded = array(
'jquery',
'jquery-core',
'jquery-migrate',
'wp-polyfill',
'wp-hooks',
'wp-i18n',
'cookie-consent',
'stripe',
'wc-checkout',
'wc-cart',
'google-recaptcha',
);
if ( in_array( $handle, $excluded, true ) ) {
return $tag;
}
// Bail if defer/async is already there, or if it is a module/inline tag.
if ( false !== strpos( $tag, ' defer' ) || false !== strpos( $tag, ' async' ) ) {
return $tag;
}
if ( false === strpos( $tag, ' src=' ) ) {
return $tag;
}
return str_replace( ' src=', ' defer src=', $tag );
}
Allow List Approach (recommended for fragile sites)
Instead of deferring everything, you defer only the handles you explicitly name. Slower gains, almost zero risk. This is what we use on client sites with WooCommerce or heavy page builders.
add_filter( 'script_loader_tag', 'ppp_defer_selected_scripts', 10, 3 );
function ppp_defer_selected_scripts( $tag, $handle, $src ) {
if ( is_admin() ) {
return $tag;
}
// Only these handles get the defer attribute.
$defer = array(
'swiper',
'lightbox',
'theme-main',
'contact-form-7',
'smooth-scroll',
);
// These get async instead (standalone, order does not matter).
$async = array(
'google-analytics',
'tawk-chat',
);
if ( in_array( $handle, $defer, true ) && false === strpos( $tag, ' defer' ) ) {
return str_replace( ' src=', ' defer src=', $tag );
}
if ( in_array( $handle, $async, true ) && false === strpos( $tag, ' async' ) ) {
return str_replace( ' src=', ' async src=', $tag );
}
return $tag;
}

Step 4: The Modern Alternative (WordPress 6.3 and Later)
Since WordPress 6.3, core supports a loading strategy natively, and it is dependency-aware: core will refuse to defer a script if one of its dependents cannot handle it. On any current WordPress install, this is the safest method available.
For scripts you enqueue yourself
wp_enqueue_script(
'theme-main',
get_stylesheet_directory_uri() . '/assets/js/main.js',
array(),
'1.0.0',
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
For scripts registered by a theme or plugin you do not control
add_action( 'wp_enqueue_scripts', 'ppp_add_defer_strategy', 100 );
function ppp_add_defer_strategy() {
$handles = array( 'swiper', 'lightbox', 'contact-form-7' );
foreach ( $handles as $handle ) {
if ( wp_script_is( $handle, 'registered' ) ) {
wp_script_add_data( $handle, 'strategy', 'defer' );
}
}
}
Which should you use?
| Method | Pros | Cons |
|---|---|---|
script_loader_tag |
Works on every WP version, can target any tag, can bulk-defer with one rule | No dependency safety net, you own the risk |
strategy => defer |
Dependency-aware, core-supported, cleaner markup | Needs WP 6.3+, handle must be registered before you call it |
Our recommendation: use the native strategy API as your default, and keep script_loader_tag for the stubborn third-party tags core cannot see or for adding async to external vendor scripts.
Step 5: Should You Ever Defer jQuery?
Short answer: only if you can prove nothing prints inline jQuery before it. Test it on staging by also moving inline scripts, or wrap the risky inline calls in a queue. If your site uses a classic theme, Elementor, WPBakery or WooCommerce, the realistic answer for 2026 is still no. The 60 to 90 ms you might save is not worth a broken mobile menu.
A better move: audit whether jQuery is even needed. Many modern block themes load it only because one legacy plugin asks for it. Remove that plugin and the problem disappears entirely.

Step 6: The Post-Defer Testing Checklist
Do not skip this. Deferring JavaScript rarely breaks the homepage, it breaks the one page nobody checks.
A. Browser console pass (5 minutes)
- Open your site in an incognito window (no caching plugin bypass, no logged-in scripts).
- Press F12 and open the Console tab.
- Hard refresh with Ctrl+Shift+R.
- Look for red errors. The three you will actually see:
Uncaught ReferenceError: $ is not definedUncaught TypeError: X is not a functionUncaught ReferenceError: jQuery is not defined
- Switch to the Network tab, filter by JS, and confirm your target scripts show the
deferattribute in their initiator chain.
B. Functional pass (10 minutes)
Click through these on both desktop and a real mobile device:
- Mobile hamburger menu, dropdowns, mega menu
- Hero slider or carousel (does it initialize, or does it flash all slides stacked?)
- Contact form submission, including validation messages and reCAPTCHA
- Lightbox / gallery / image zoom
- Accordions, tabs, FAQ toggles
- Search overlay and AJAX search
- WooCommerce: variation selector, add to cart, mini-cart update, checkout
- Cookie banner and any consent-gated script
- Sticky header behaviour on scroll
- Anything in your footer: maps, newsletter popups, chat widget
C. PageSpeed Insights pass
- Purge all caches (plugin, server, CDN) before measuring.
- Run PageSpeed Insights on the homepage, a blog post, and a product or service page.
- Compare against your baseline on these specific metrics:
- Total Blocking Time (TBT) – should drop the most
- Largest Contentful Paint (LCP) – should drop if scripts were in the head
- “Eliminate render-blocking resources” – deferred scripts should disappear from the list
- Cumulative Layout Shift (CLS) – watch this one. If it got worse, a deferred script is now resizing elements late. That script belongs on the exclusion list.
- Run the test two or three times. Lab data fluctuates, do not chase a single number.
- Check Core Web Vitals field data in Search Console after 28 days to confirm the gain is real for actual users.
Troubleshooting: Symptom to Fix Table
| Symptom | Likely cause | Fix |
|---|---|---|
| “$ is not defined” in console | jQuery deferred, inline jQuery runs first | Add jquery and jquery-core to the exclusion list |
| Slider shows all slides stacked | Init script executed before library | Defer both the library and its init, or exclude both |
| CLS went up | Deferred script resizes above-fold elements | Exclude it, or reserve space in CSS |
| Form submits but no validation | Form plugin script deferred past its inline config | Exclude the form handle |
Tag appears with defer defer |
Cache plugin and your snippet both acting | Disable one of them, keep the strpos guard |
| Admin bar or editor broken | Filter running in admin context | Keep the is_admin() and is_customize_preview() checks |

Manual Method vs Plugin: Be Honest About the Trade-Off
Plugins that defer JavaScript are convenient and most of them use the exact same script_loader_tag filter under the hood. The manual method wins when:
- You want zero extra overhead and no new dependency.
- You need surgical control per handle, per template, or per device.
- You are already shipping a custom theme or a site-specific plugin.
- Your optimization plugin’s defer feature is breaking something and you need granularity it does not offer.
A plugin wins when the site is managed by a non-developer, or when you also want delayed-until-interaction loading, which is a separate (and more aggressive) technique than defer.
Bonus: Conditional Deferring Per Template
You can defer a script on blog posts but leave it alone on the shop. Just wrap your logic in conditional tags:
add_filter( 'script_loader_tag', 'ppp_conditional_defer', 10, 3 );
function ppp_conditional_defer( $tag, $handle, $src ) {
if ( is_admin() ) {
return $tag;
}
// Never defer anything inside the WooCommerce funnel.
if ( function_exists( 'is_woocommerce' ) && ( is_cart() || is_checkout() || is_product() ) ) {
return $tag;
}
$defer = array( 'swiper', 'lightbox', 'theme-main' );
if ( in_array( $handle, $defer, true ) && false === strpos( $tag, ' defer' ) ) {
return str_replace( ' src=', ' defer src=', $tag );
}
return $tag;
}
FAQ
Does deferring JavaScript improve SEO rankings?
Not directly. It improves Core Web Vitals, specifically TBT, INP and often LCP, and page experience is a ranking signal. The real benefit is user-side: faster interaction means lower bounce and better conversions.
Is it safe to defer jQuery in WordPress?
Usually not on a site using classic themes, page builders or WooCommerce, because inline jQuery() calls execute before the deferred file loads. Keep jquery, jquery-core and jquery-migrate on your exclusion list unless you have verified there are zero inline dependencies.
What is the difference between defer and “delay JavaScript execution”?
defer is a native HTML attribute: the script still loads and runs on page load, just after HTML parsing. “Delay until interaction” is a plugin technique that holds scripts until the user scrolls, moves the mouse or taps. Delay gives bigger lab scores but carries much higher breakage risk, especially for analytics and consent tools.
Where exactly do I put this code?
In your child theme’s functions.php, or better, in a small site-specific plugin in wp-content/plugins/. Avoid parent themes (lost on update) and avoid pasting into core files.
My script already loads in the footer. Should I still defer it?
Footer scripts are not render-blocking for the initial HTML, so the LCP gain is small. However defer still lets the browser download them in parallel earlier while parsing continues, which can shave Total Blocking Time. Test it and keep what measures better.
Why do some scripts not get the defer attribute even after adding the filter?
Three common reasons: the tag is printed inline (no src attribute, so defer does nothing), the script is output by a plugin outside the wp_enqueue_script system, or a concatenation/optimization plugin rewrites the tag after your filter runs. Check the rendered HTML source to confirm.
Can I use defer and async on the same script?
You can technically write both, but browsers that support async will ignore defer. Pick one per script. Use async only for standalone third-party tags.
Wrapping Up
Learning how to defer JavaScript in WordPress manually is less about the snippet and more about the discipline around it: identify handles, protect the critical ones, apply defer, then verify with the console and PageSpeed Insights before you call it done.
Start with the allow-list version. Add two or three handles, test, measure, then expand. A site that loads 150 ms slower but works perfectly will always outperform a 100/100 score that breaks checkout.
