WordPress at Your Fingertips

WP 6.3: async and defer attributes when registering scripts

In WordPress 6.3, support for registering scripts with the async and defer attributes has been introduced as part of the enhancement of the existing Scripts API. This addresses a long-standing issue from ticket by adding a script loading strategy.

The following strategies are supported:

  • Blocking - the script blocks the page load. This is the default behavior.
  • Deferred - using the defer strategy.
  • Asynchronous - using the async strategy.

Why is this needed?

Adding defer or async to script tags allows scripts to be loaded without "blocking" the rest of the page load, leading to improved site performance by enhancing the Largest Contentful Paint (LCP) metric. Read more here.

Previously, developers had to resort to less than ideal alternatives such as:

All of these approaches are considered "bad" because they do not take into account script dependencies, which can lead to compatibility issues or errors when working with other scripts.

Read about the difference between defer and async in the article: Difference between async and defer in the script tag.

In short, it is as follows:

  • Deferred scripts: Scripts with the defer attribute are executed only after the full DOM tree has been loaded (but before the DOMContentLoaded event). Deferred scripts, unlike asynchronous ones, are executed in the order they were added to the DOM.

  • Asynchronous scripts: Scripts with the async attribute are loaded without blocking the page (the browser does not wait for the script to load) and are executed immediately after loading. Asynchronous scripts are not executed in the order they appear in the page's code, but rather in the order they are loaded. For example, script B (which appears in the code after script A) may be executed first because the browser loaded it before script A. Such scripts do not wait for the DOM tree to be built (they do not wait for the DOMContentLoaded event) and are executed as soon as the browser has loaded them.

What has been changed in WordPress

In general terms, the changes can be described as follows:

  • WordPress now allows specifying how a script should be included - which strategy to use. This is specified for the functions wp_register_script() and wp_enqueue_script().

  • These functions have new signatures - the type of the parameter $in_footer has changed. Now, instead of a boolean type, an array is used, in which you can specify where and how the script should be loaded. Backward compatibility with the bool type is maintained.

  • The loading strategy can also be specified through wp_script_add_data(). This can be useful for backward compatibility of the code.

  • Additions and improvements have been made to the WP_Scripts{} class to specify the script loading strategy.

Example: defer script inclusion in the header

The loading strategy is specified by passing a key-value pair of the strategy in the $args parameter (formerly $in_footer).

wp_register_script(
	'my_script',
	'https://example.com/path/to/my_script.js',
	[],
	'1.0',
	[
		'strategy' => 'defer'
	]
);
wp_register_script(
	'my_script',
	'https://example.com/path/to/my_script.js',
	[],
	'1.0',
	[
		'in_footer' => true,
		'strategy'  => 'async',
	]
);

The same applies to wp_enqueue_script().

Implementation Details

This function takes into account the script's dependency tree (its dependencies and/or dependent components) when choosing the "appropriate strategy" to avoid applying a strategy suitable for one script but harmful to others in the tree, causing unintended execution order violations. This was practically impossible to achieve with the previous methods of adding script loading strategy attributes when using the alternative "bad" methods described above.

The technical implementation of script loading was done without breaking the existing script API. Enhancements were made to all script API functions, such as wp_register_script() and wp_enqueue_script().

About Dependencies

To avoid confusion, let's clarify the difference between script dependencies and dependent scripts:

  • Dependent scripts - these are scripts that depend on the current script, i.e., scripts that automatically include the current script before their inclusion because they depend on it.

  • Dependencies - on the other hand, are scripts on which the current script depends, i.e., they must be called before the script is called.

The most noticeable change to the existing wp_register_script() and wp_enqueue_script() functions is the change in the function signature, where the $in_footer parameter (previously a boolean parameter) has been overloaded to accept an array $args with any of the following keys:

in_footer (bool)
Behaves the same as the previous implementation of the $in_footer parameter.
strategy (string)

Indicates how the script should be loaded (by which strategy). Currently, two values are available: defer and async for deferred and asynchronous scripts, respectively.

The default behavior is blocking, maintaining backward compatibility with existing script registrations and calls.

wp_script_add_data() - Maintaining backward compatibility

For previous/existing usage of the wp_register_script() and wp_enqueue_script() functions using the boolean parameter $in_footer, full backward compatibility is maintained. Therefore, this enhancement does not break the API.

Although the changes are not critical, when using the new $args parameter in a plugin/theme/codebase running on WordPress < 6.3, there is one scenario where $in_footer will be misunderstood by the core.

For example, in WordPress >= 6.3, the following array for $in_footer will work according to the new logic. The script will be included in the header with the defer attribute.

However, in WordPress versions < 6.3, such an array for $in_footer will be interpreted as a boolean value true, and the script will be included in the footer, even though the developer specified that the script should be included in the header. However, it can be argued that in versions of WordPress that do not support deferred/asynchronous scripts, outputting the script in the footer is even better than outputting it in the header.

The simplest way to prevent such compatibility issues is to pass the strategy not through the $args parameter, but through the wp_script_add_data() function:

wp_register_script(
	'my_script',
	'https://example.com/path/to/my_script.js',
	[],
	'1.0.0',
	false
);

wp_script_add_data( 'foo', 'strategy', 'defer' );

Another option, when dependencies need to be handled, is to create a custom wrapper function for registering/enqueuing scripts that takes into account the changes in WordPress 6.3.

An example of such a wrapper for registering/enqueuing scripts might look like this:

myplugin_register_script( $handle, $src, $deps, $ver, $args ) {
	global $wp_version;

	if ( version_compare( $wp_version,'6.3', '>=' ) ) {
		wp_register_script( $handle, $src, $deps, $ver, $args );
	} else {
		$in_footer = isset( $args['in_footer'] ) ? $args['in_footer'] : false;

		wp_register_script( $handle, $src, $deps, $ver, $in_footer );
	}
}

Changing the specified strategy due to dependencies

Despite the developer specifying a specific loading strategy, the final strategy may differ from the specified one. This depends on the script's dependencies and inline scripts.

For example, if a developer registers the script foo with a defer strategy, to maintain the intended execution order, the dependencies of the script should use the defer or blocking strategy, and dependent scripts should use the defer strategy. If a script dependent on foo is registered with a blocking strategy, then the script foo and all its dependencies will automatically become blocking, regardless of the specified defer strategy. This can affect the dependency tree of the base script, and all scripts in this tree may also be interpreted as blocking.

The new logic in the WP_Scripts class is responsible for performing a series of logical checks to ensure that the final strategy for a given script is the most suitable, based on the factors described above.

A handler will not inherit a strategy that is "stricter" than intended, i.e., a script marked for deferred loading will never be changed to asynchronous, but the reverse result is permissible if external factors allow it.

Inline scripts

There are some nuances when applying the loading strategy for scripts that depend on inline scripts, as this affects the final application of the chosen strategy.

Inline scripts registered in the "before" position are almost always unchanged in their behavior because they will be included before any of the strategies - blocking, defer, or async - are applied to the parent/main script.

However, inline scripts registered in the "after" position (the default for wp_add_inline_script()) will affect the final loading strategy of the parent/main script if the parent/main script has an async or defer execution strategy.

Thus, if an inline script is included in the "after" position, the parent/main script will be considered blocking - the specified defer or async strategy will be canceled, and a blocking strategy will be used. This, in turn, can affect the dependency tree of the base script, and all scripts in this tree may also be interpreted as blocking.

For further discussions on this topic and possible changes, an additional ticket #58632 has been opened.

Switching to the new API

Implementations of code using deprecated methods to add attributes async or defer to script tags should switch to the new API. This includes scripts where attributes were added to the script tag through the script_loader_tag filter or, even worse, through the clean_url filter.

The following examples demonstrate a possible implementation using the script_loader_tag and clean_url filters. Such implementations must be migrated to the new API:

Example 1: Adding the defer attribute through script_loader_tag

add_filter( 'script_loader_tag', 'old_approach', 10, 2 );

function old_approach( $tag, $handle ) {

	if ( 'foo' !== $handle ) {
		return $url;
	}

	return str_replace( ' src=', ' defer src=', $tag );
}

Example 1: Adding the defer attribute through clean_url

add_filter( 'clean_url', 'old_brittle_approach' );

function old_brittle_approach( $url ) {

	if ( false === strpos( $url, 'foo.js' ) ) {
		return $url;
	}

	return "$url' defer ";
}

If you are using an approach similar to the one described above to add the defer or async attributes to a script, switch to the new API using any of the approaches described earlier in this message.

--

Source: https://make.wordpress.org/core/2023/07/14/registering-scripts-with-async-and-defer-attributes-in-wordpress-6-3/

1 comment
    Log In