WordPress 6.3 introduces support for registering scripts with async
and defer
attributes as part of an enhancement Enhancements are simple improvements to WordPress, such as the addition of a hook, a new feature, or an improvement to an existing feature. to core Core is the set of software required to run WordPress. The Core Development Team builds WordPress.’s existing Scripts API An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways.. This addresses a long-standing Trac ticket, and adds the ability to define a loading strategy for scripts. Supported strategies are as follows:
- Blocking (default, this strategy is not supplied)
- Deferred (by supplying a defer loading strategy)
- Asynchronous (by supplying an async loading strategy)
This enhancement was originally proposed in December 2022.
Why is this enhancement useful?
Adding defer
or async
to script tags enables script loading without “blocking” the rest of the page load, resulting in more performant sites via improved Largest Contentful Paint (LCP) performance. This leads to a better user experience. This has been common practice in web engineering for over a decade, yet to date there have been no core methods or a means of achieving this when registering/enqueuing scripts using core WordPress APIs.
Prior to this enhancement, developers have had to resort to less than ideal alternatives such as directly filtering the tags at the point of output (using the script_loader_tag
filter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output., or worse the clean_url
filter), or handling the tag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.) output directly using wp_print_script_tag
and the wp_script_attributes
filter. Although fairly common practice (as it was the only means available prior), it is considered “hacky” as it does not take into account the dependency tree, or inline scripts for that matter, which can lead to interoperability issues or bugs with other scripts.
The difference between deferred (via the defer
script attribute) and asynchronous (via the async
script attribute) scripts is as follows:
- Deferred scripts
Scripts marked for deferred execution — via the defer
script attribute — are only executed once the DOM tree has fully loaded (but before the DOMContentLoaded and window load events). Deferred scripts are executed in the same order they were printed/added in the DOM, unlike asynchronous scripts.
- Asynchronous scripts
Scripts marked for asynchronous execution — via the async
script attribute — are executed as soon as they are loaded by the browser. Asynchronous scripts do not have a guaranteed execution order, as script B (although added to the DOM after script A) may execute first given that it may complete loading prior to script A. Such scripts may execute either before the DOM has been fully constructed or after the DOMContentLoaded event.
Summary of the changes
At a high level, the changes can be summarized as follows:
- WordPress now adds support for specifying a script loading strategy via the
wp_register_script()
and wp_enqueue_script()
functions.
- These functions have new function signatures, with the prior
$in_footer
boolean parameter being overloaded to accept a new $args
array parameter in order to facilitate an easy entry point for specifying a loading strategy for a script, while still retaining full backward compatibility for $in_footer
implementations; this retains the means of specifying whether a script should be printed in the footer or not via a key within the new $args
parameter. Note that the strategy can also be specified in a backwards-compatible way via wp_script_add_data()
.
Various additions and enhancements to the WP_Scripts
class were made to facilitate the necessary business logic that prepares and outputs a script’s loading strategy.
Example 1: Specifying a loading strategy for a script
A loading strategy may be assigned via the wp_register_script()
and wp_enqueue_script()
functions by passing a strategy key value pair to the new/overloaded $args
parameter.
The following example showcases a new script with handle 'foo'
being registered as a deferred script:
wp_register_script(
'foo',
'/path/to/foo.js',
array(),
'1.0.0',
array(
'strategy' => 'defer'
)
);
This exact same means of specifying a loading strategy may be achieved via the wp_enqueue_script()
function.
Example 2: Specifying that a script be printed in the footer via the new API
The next example showcases a second script being specified for footer printing using the new API, while also supplying the async
loading strategy at the same time:
wp_register_script(
'bar',
'/path/to/bar.js',
array(),
'1.0.0',
array(
'in_footer' => true,
'strategy' => 'async',
)
)
This exact same means of specifying footer printing may be achieved via the wp_enqueue_script()
function.
Implementation details
This feature enhances the existing Scripts API by providing a simple means of specifying a loading strategy by extending commonly used & well known aspects of the Scripts API. It also takes into consideration a script’s dependency tree (its dependencies and/or dependents) when deciding on an “eligible strategy” so as not to result in application of a strategy that is valid for one script but detrimental to others in the tree by causing an unintended out of order of execution. This is near-impossible to achieve via the prior means of adding script loading strategy attributes when using the alternative “hacky” means outlined in the section above.
Technical implementation of the script loading strategy enhancements have been undertaken within the existing Scripts API, notably within the WP_Scripts
class, and via enhancements to the familiar and commonly used wp_register_script()
and wp_enqueue_script()
functions.
A note on dependencies vs dependents
To avoid confusion regarding terminology, let’s clarify the difference between a script’s dependencies vs. its dependents. A script’s dependencies refers to the scripts that said script itself depends on, i.e they must be enqueued prior to said script being enqueued. A script’s dependents on the other hand refers to the scripts that depend on said script, i.e scripts that define said script in their dependencies array.
Changes to the $in_footer
parameter of wp_register_script()
and wp_enqueue_script()
functions
The most notable change to the existing wp_register_script()
and wp_enqueue_script()
functions is the function signature change, where $in_footer
(previously a boolean parameter) has been overloaded to also accept an array $args
parameter, with any of the following keys:
(bool) in_footer
- Behaves just like prior implementation of the top level
$in_footer
param.
(string) strategy
- Accepts an intended loading strategy for the given script being registered/enqueued. Acceptable string values available at the time of implementation are
defer
for deferred scripts and async
for asynchronous scripts.
- Defaults to blocking behavior, thus retaining backward compatibility for existing script registrations and enqueues.
Retaining backward compatibility
For prior/existing usage of the wp_register_script()
and wp_enqueue_script()
functions making use of the $in_footer
boolean param, backward compatibility is retained via logic that explicitly sets the scripts group to the applicable value for footer or header The header of your site is typically the first thing people will experience. The masthead or header art located across the top of your page is part of the look and feel of your website. It can influence a visitor’s opinion about your content and you/ your organization’s brand. It may also look different on different screen sizes. printing based on the boolean value passed to the new/overloaded $args
parameter. Thus, full backward compatibility is retained and this is a non-breaking enhancement of the API.
While the changes introduced within this feature themselves are considered non-breaking, when making use of the new $args
parameter (replacing/overloading the previous $in_footer
parameter) in a plugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party/theme/codebase powered by WordPress <6.3, there is one scenario where the $in_footer
intention would be misunderstood by core. Take for example the following scenario:
wp_register_script(
'foo',
'/path/to/foo.js',
array(),
'1.0.0',
array(
'strategy' => 'defer',
'in_footer' => false, // Note: This is the default value.
)
);
In WordPress >=6.3 this would be correctly evaluated as being printed in the head via the in_footer
array key value being false (which is also the default).
In WordPress versions <6.3, however, the presence of the above array assigned to the $in_footer
parameter would itself evaluate to a boolean value of true
, the opposite of what may be intended by the developer. That being said, one could rightfully argue that in versions of WordPress that do not support deferred/asynchronous scripts, having them printed in the footer is the next best alternative.
The simplest way to prevent this interoperability problem is to pass the strategy via a different means than the $args
param to wp_register_script()
or wp_enqueue_script()
functions.
It can be passed instead via wp_script_add_data()
, in which case it will be understood by WordPress 6.3 but ignored by older versions.
wp_register_script(
'foo',
'/path/to/foo.js',
array(),
'1.0.0',
false
);
wp_script_add_data( 'foo', 'strategy', 'defer' );
Alternatively, interoperability between WordPress versions newer and older than 6.3 using a single function call per script can be achieved by wrapping script registrations/enqueues within a wrapper function that accounts for new and old function signatures, thus retaining total backward compatibility.
An example of such a script registration/enqueue wrapper may look something as follows:
myplugin_register_script( $handle, $src, $deps, $ver, $args ) {
global $wp_version;
// If >= 6.3, re-use wrapper function signature.
if ( version_compare( $wp_version,'6.3', '>=' ) ) {
wp_register_script(
$handle,
$src,
$deps,
$ver,
$args
);
} else {
// Extract in_footer value for older version usage.
$in_footer = isset( $args['in_footer'] ) ? $args['in_footer'] : false;
wp_register_script(
$handle,
$src,
$deps,
$ver,
$in_footer
);
}
}
Intended vs. eligible loading strategies
It should be noted that while a developer may intend for a given script to contain a certain loading strategy, the final loading strategy may differ based on factors such as script dependencies/dependents and inline scripts.
For example, if a developer registers script foo with a strategy of defer, its dependencies must either use a defer or blocking strategy and its dependents must use a defer strategy in order for the intended execution order to be maintained. If a dependent of said script foo is then registered with a blocking intended strategy, script foo and all of its dependencies would then become blocking.
Newly added logic within the WP_Scripts
class is responsible for exercising a series of logical checks that ensure that the final strategy for a given script handle is the most eligible strategy based on the factors outlined further above.
A handle will never inherit a strategy that is “more strict” than the one intended, i.e. a script marked for deferred loading will never be changed to asynchronous loading, but the reverse may indeed be the outcome if environmental factors warrant it.
Inline scripts
There are some nuances when applying a loading strategy to scripts (or scripts in the dependency tree) that have inline scripts attached to them, as this ultimately has an effect on the final outcome of an intended/eligible strategy.
Inline scripts that are registered in the before position, remain largely unchanged in behavior, given that they will inherently be parsed and/or executed before the main/parent script is parsed for immediate, deferred or asynchronous execution.
Inline scripts that are registered in the after position (the default for wp_add_inline_script()
), however, will affect the final loading strategy of a main/parent script if said main/parent script has an async or deferred eligible strategy. This is largely due to the complexity in ensuring that inline scripts attached to deferred/asynchronous scripts execute at the appropriate and expected time, while not having a negative impact on the parent script itself, and the dependency tree as a whole.
Therefore, if a given script handle contains inline scripts in the after position, this script will be assumed to be blocking and any intended strategy such as defer/async will be removed, with the final eligible strategy being blocking. This, in turn, may affect the script’s dependency tree, and all scripts within it, may too, be treated as blocking scripts in a bid to retain correct execution order and functionality of the enqueued scripts. Logic is explicitly employed to ensure correct execution order of the script tree in these instances.
A follow-up ticket Created for both bug reports and feature development on the bug tracker. #58632 has been opened to continue ongoing discussions and proof of concept implementations to potentially introduce delayed execution of inline after scripts in the future which would preserve their loading order.
Migrating to the new API
Code implementations making use of legacy methods of adding async
or defer
attributes to script tags should migrate to the new API. These include scenarios where the attributes have historically been added to a script tag by way of the script_loader_tag
filter, or worse via the clean_url
filter.
The following examples show implementation that make use of the script_loader_tag
and clean_url
filters, which are now considered less-than-ideal approaches, and then show how it should be done using the new API:
Migration Moving the code, database and media files for a website site from one server to another. Most typically done when changing hosting companies. Consideration Example 1: Adding a defer
attribute via the script_loader_tag
filter
function old_approach( $tag, $handle ) {
// Only affects foo script.
if ( 'foo' !== $handle) {
return $url;
}
// Modern implementations may employ WP_HTML_Tag_Processor here.
return str_replace( ' src=', ' defer src=', $tag );
}
add_filter( 'script_loader_tag', old_approach, 10, 2 );
Migration Consideration Example 2: Adding a defer
attribute via the clean_url
filter
// WARNING: THIS HAS ALWAYS BEEN BRITTLE AND IS NOT RECOMMENDED.
function old_brittle_approach( $url ) {
// Only affects foo script.
if ( false === strpos( $url, 'foo.js' ) ) {
return $url;
}
return "$url' defer "; // Assumes single-quoted attributes!
}
add_filter( 'clean_url', 'old_brittle_approach' );
If you are using an approach similar to the one above to add defer
or async
attributes to a script, please migrate to the new API by using any one of the approaches outlined earlier in this post.
—
Props: @joemcgill @flixos90 @westonruter @adamsilverstein @jyolsna @stevenlinx
#6-3, #dev-notes, #dev-notes6-3