Defer/Async JavaScript for Better Performance
Large or render-blocking JavaScript can slow down your WordPress site. Two safe, high-impact techniques are defer and async. This guide explains when to use each and how to implement them properly in WordPress (including modern strategy support and backward-compatible methods).
Defer vs Async: What’s the Difference?
defer: The browser downloads the script in parallel but executes it after HTML parsing, in document order. Best when your scripts depend on each other or on DOM structure.async: The browser downloads the script in parallel and executes it as soon as it finishes, out of order. Best for independent, third-party scripts (analytics, ads, widgets) that don’t affect layout logic.
Rule of thumb: If the script has dependencies or needs the DOM ready, use defer. If it’s fire-and-forget, use async.
Baseline: Load Scripts in the Footer
Before tweaking attributes, make sure non-critical scripts load in the footer to reduce blocking in the head:
<?php
add_action( 'wp_enqueue_scripts', function () {
// Example: load theme.js in footer (the 5th param or 'in_footer' => true)
wp_enqueue_script(
'theme-scripts',
get_template_directory_uri() . '/assets/js/theme.js',
array(), // dependencies
'1.0.0',
true // in_footer
);
} );
Method 1 (WP 6.3+): Use the strategy Argument
WordPress 6.3 introduced a first-class way to set loading strategy per script. Prefer this API if your site runs on WP 6.3 or later.
<?php
add_action( 'wp_enqueue_scripts', function () {
// Defer a dependent bundle (keeps execution order)
wp_enqueue_script(
'app-bundle',
get_template_directory_uri() . '/assets/js/app.bundle.js',
array( 'wp-element' ), // example dependency
'1.2.3',
array(
'in_footer' => true,
'strategy' => 'defer', // 'defer' or 'async'
)
);
// Async a third-party script (no dependencies)
wp_enqueue_script(
'awesome-analytics',
'https://cdn.example.com/awesome-analytics.min.js',
array(),
null,
array(
'in_footer' => true,
'strategy' => 'async',
'crossorigin' => 'anonymous', // optional if required
)
);
} );
Method 2 (Backward-Compatible): wp_script_add_data()
For WordPress versions before 6.3, add attributes via wp_script_add_data(). Use this when you can’t rely on strategy.
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'theme-scripts',
get_template_directory_uri() . '/assets/js/theme.js',
array( 'jquery' ),
'1.0.0',
true
);
// Defer keeps order with dependencies
wp_script_add_data( 'theme-scripts', 'defer', true );
// Independent third-party: async
wp_enqueue_script(
'maps-api',
'https://example.com/maps.js',
array(),
null,
true
);
wp_script_add_data( 'maps-api', 'async', true );
} );
Method 3 (Bulk): Filter to Defer Most Scripts
If you want to defer everything except a short allow-list, use script_loader_tag. Be careful: over-eager deferring can break inline code that expects libraries immediately.
<?php
add_filter( 'script_loader_tag', function ( $tag, $handle, $src ) {
$exclude = array(
'jquery', // often needed early by legacy themes
'wp-polyfill', // core polyfills
'contact-form-7', // example plugin handle
);
if ( in_array( $handle, $exclude, true ) ) {
return $tag; // do not alter
}
// Add defer if not already async/defer and it's a normal script
if ( false === strpos( $tag, 'defer' ) && false === strpos( $tag, 'async' ) ) {
$tag = str_replace( '<script ', '<script defer ', $tag );
}
return $tag;
}, 10, 3 );
Inline Scripts That Depend on a Handle
When you add inline code that relies on a handle (e.g., your bundle exposes a global), attach inline code after that handle so it runs in the right order—even with defer:
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'app',
get_template_directory_uri() . '/assets/js/app.js',
array(),
'1.0',
true
);
wp_script_add_data( 'app', 'defer', true );
$inline = 'window.App && window.App.init && window.App.init();';
wp_add_inline_script( 'app', $inline, 'after' );
} );
ES Modules: type="module" Is Deferred by Default
Module scripts are parsed as modules and are effectively deferred by default. You can combine this with a nomodule fallback for legacy browsers if needed.
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'module-app',
get_template_directory_uri() . '/assets/js/app.module.js',
array(),
'1.0.0',
array(
'in_footer' => true,
'type' => 'module', // WP will output type="module"
)
);
// Optional: nomodule fallback for old browsers
wp_enqueue_script(
'nomodule-app',
get_template_directory_uri() . '/assets/js/app.legacy.js',
array(),
'1.0.0',
true
);
wp_script_add_data( 'nomodule-app', 'nomodule', true );
} );
Preload + Defer for Critical Bundles
If a deferred bundle is critical for interaction, you can hint the network earlier using preload, then execute with defer:
<?php
add_action( 'wp_head', function () {
$src = esc_url( get_template_directory_uri() . '/assets/js/app.bundle.js' );
echo '<link rel="preload" as="script" href="' . $src . '">';
}, 1 );
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'app-bundle',
get_template_directory_uri() . '/assets/js/app.bundle.js',
array(),
'1.0.0',
true
);
wp_script_add_data( 'app-bundle', 'defer', true );
} );
When Not to Use Async/Defer
- Inline scripts in the head that expect a library (e.g., jQuery) to be available immediately.
- A/B testing snippets that must run before rendering to avoid layout flicker.
- Scripts that intentionally block rendering for UX reasons (rare).
Compatibility Tips
- Dependencies: Use
deferwhen scripts depend on each other; avoidasyncin dependency chains. - jQuery-heavy themes: Keep jQuery in the head or defer it together with any code that uses it (attach inline with
wp_add_inline_script). - Third-party: Prefer
async. Addcrossoriginandreferrerpolicyif the provider requires it. - Measure: Verify improvements with Lighthouse/PageSpeed Insights. Watch First Contentful Paint, Largest Contentful Paint, and Total Blocking Time.
Quick Checklist
- Move non-critical scripts to the footer.
- Use
strategy: 'defer'(WP 6.3+) for theme/app bundles. - Use
strategy: 'async'for independent third-party scripts. - Attach inline code with
wp_add_inline_script( 'handle', $code, 'after' ). - Optionally
preloadcritical deferred bundles. - Test on real devices and roll back handles that break.
Conclusion
Smart use of defer and async can significantly reduce render-blocking and improve Core Web Vitals. On modern WordPress, prefer the strategy argument for clarity and safety; otherwise, use wp_script_add_data(). Start with defer for your bundles and async for third-party scripts, then validate the impact with performance audits.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.