A lot of themes still load every script in the <head>, so the browser has to download and run each one before it can finish rendering the page. The old workaround was a script_loader_tag filter that string-replaced <script with <script defer.
WordPress 6.3 added a proper way to do this. The fifth argument of wp_enqueue_script(), which used to be only true or false for footer loading, now also accepts an array with a loading strategy.
1. Defer a script you enqueue
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'hsr-slider',
get_stylesheet_directory_uri() . '/js/slider.js',
array(),
'1.0.0',
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
} );
strategy takes defer or async. Both let the browser keep building the page while the script downloads, which a normal script tag doesn’t.
The difference is when they run. A deferred script waits until the HTML is fully parsed, and deferred scripts run in the order they were added. An async script runs the moment it finishes downloading, so there’s no guaranteed order.
I use defer for almost everything. async only makes sense for scripts that don’t depend on anything and nothing depends on them, like some analytics tags.
2. Defer a script from a plugin
You don’t need to edit the plugin. Find the script’s handle in the page source (it’s the id on the script tag, without the -js at the end) and set the strategy after the plugin registers it:
add_action( 'wp_enqueue_scripts', function () {
wp_script_add_data( 'some-plugin-script', 'strategy', 'defer' );
}, 20 );
wp_script_add_data() attaches extra data to a script that’s already registered. It takes three things: the handle, a key, and a value. When you pass strategy in the enqueue array like in snippet 1, WordPress stores it this same way behind the scenes, so both snippets end up in the same place.
The catch is that it only works on a registered handle. If the plugin hasn’t registered its script yet when your code runs, the function returns false and nothing happens.
That’s why the priority is 20. It makes this run after the plugin’s own enqueue, which usually sits at the default 10.
If you’re not sure the timing is right, check the return value while testing:
add_action( 'wp_enqueue_scripts', function () {
if ( ! wp_script_add_data( 'some-plugin-script', 'strategy', 'defer' ) ) {
error_log( 'some-plugin-script is not registered yet' );
}
}, 20 );
If that line shows up in debug.log, the plugin registers its script later than priority 20 (some use a higher priority or a different hook), so bump the number up and try again.
3. Check that it worked
View source and find the script tag. If WordPress applied the strategy, you’ll see defer data-wp-strategy="defer" on it.
If the attribute is missing, look at what depends on that script. WordPress won’t defer a script when something depending on it still loads normally, and it falls back without any notice.
Warning: leave jQuery out of this.
Plenty of themes and plugins print inline code in the page that calls jQuery(...) right away. If jQuery is deferred, that code runs before jQuery exists.
You’ll see it as jQuery is not defined in the browser console, usually along with a slider or menu that quietly stops working.
That’s all for this one.
Try it: pick one non-jQuery script loading in your homepage <head> and defer it with snippet 2. If it didn’t get the data-wp-strategy attribute, tell me which plugin’s script held it back.