Skip to content

Instantly share code, notes, and snippets.

@adamsilverstein
Last active August 9, 2023 21:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save adamsilverstein/ec18b67a72ff74dec12624e989e23142 to your computer and use it in GitHub Desktop.
Save adamsilverstein/ec18b67a72ff74dec12624e989e23142 to your computer and use it in GitHub Desktop.
WordPress `wp_enqueue_script`: use `async` or `defer` in a backwards compatible manner
<?php
/**
* Register scripts with a `defer` or `async` strategy in a backwards compatible manner.
*
* From WordPress 6.3 onwards, the `wp_register_script` function accepts an `$args` array that
* can include a `strategy` key with a value of either `async` or `defer`.
*
* This helper function handles the backwards compatibility for older versions of WordPress. When a
* `strategy` key is present in the `$args` array (and is either `defer` or `async`), the
* `script_loader_tag` filter is used to add the attribute to the script tag.
*
* Note that for older versions of WordPress, dependencies is not considered - the attribute is added unconditionally.
*
* When support for WP<6.3 is no longer required, simply replace all instances of this function with
* `wp_register_script()`.
*
* @see wp_register_script()
*
* @param string $handle Name of the script. Should be unique.
* @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory.
* If source is set to false, script is an alias of other scripts it depends on.
* @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array.
* @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version.
* If set to null, no version is added.
* @param array|bool $args {
* Optional. An array of additional script loading strategies. Default empty array.
* Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false.
*
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* }
* @return bool Whether the script has been registered. True on success, false on failure.
*/
function wpnext_register_script( $handle, $src, $deps, $ver, $args ) {
// If >= 6.3, re-use wrapper function signature.
if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' ) ) {
wp_register_script(
$handle,
$src,
$deps,
$ver,
$args
);
} else {
wp_register_script(
$handle,
$src,
$deps,
$ver,
isset( $args['in_footer'] ) ? $args['in_footer'] : false
);
if ( isset( $args['strategy'] ) ) {
wp_script_add_data( $handle, 'strategy', $args['strategy'] );
}
}
}
/**
* Enqueue scripts with a `defer` or `async` strategy in a backwards compatible manner.
*
* From WordPress 6.3 onwards, the `wp_enqueue_script` function accepts an `$args` array that
* can include a `strategy` key with a value of either `async` or `defer`.
*
* This helper function handles the backwards compatibility for older versions of WordPress. When a
* `strategy` key is present in the `$args` array (and is either `defer` or `async`), the
* `script_loader_tag` filter is used to add the attribute to the script tag. Note that
* for older versions of WordPress, dependency is not managed and the attribute is added unconditionally.
*
* When support for WP<6.3 is no longer required, simply replace all instances of this function with
* `wp_enqueue_script()`.
*
* @see wp_enqueue_script()
*
* @param string $handle Name of the script. Should be unique.
* @param string $src Full URL of the script, or path of the script relative to the WordPress root directory.
* Default empty.
* @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array.
* @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version.
* If set to null, no version is added.
* @param array|bool $args {
* Optional. An array of additional script loading strategies. Default empty array.
* Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false.
*
* @type string $strategy Optional. If provided, may be either 'defer' or 'async'.
* @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'.
* }
*/
function wpnext_enqueue_script( $handle, $src, $deps, $ver, $args ) {
wpnext_register_script( $handle, $src, $deps, $ver, $args );
wp_enqueue_script( $handle );
}
if ( version_compare( get_bloginfo( 'version' ), '6.3', '<' ) ) {
add_filter(
'script_loader_tag',
static function( $tag, $handle ) {
$strategy = wp_scripts()->get_data( $handle, 'strategy' );
if ( in_array( $strategy, array( 'async', 'defer' ), true ) && false === strpos( $tag, $strategy) ) {
$tag = str_replace( '<script ', '<script ' . $strategy . ' ', $tag );
}
return $tag;
},
10,
2
);
}
@westonruter
Copy link

Personally I would opt to not create wrapper functions for wp_enqueue_script() and wp_register_script(), although I know @felixarntz prefers for that approach to make it easier to migrate in the future. For themes and plugins that want to add back-compat for WP<6.3, I think they can just rather call wp_script_add_data() to set the strategy like as follows:

add_action(
	'wp_enqueue_scripts',
	static function () {
		wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '0.1', true );
		wp_script_add_data( 'foo', 'strategy', 'defer' );
	}
);

The wp_script_add_data( 'foo', 'strategy', 'defer' ) call here is fully compatible with WP 6.3+, and in older versions it does nothing. So for the older versions, then instead of there being many script_loader_tag filters added, there can just be the one that grabs that script data back out and injects the attribute:

if ( version_compare( get_bloginfo( 'version' ), '6.3', '<' ) ) {
	add_filter(
		'script_loader_tag',
		static function( $tag, $handle ) {
			$strategy = wp_scripts()->get_data( $handle, 'strategy' );
			if ( in_array( $strategy, array( 'async', 'defer' ), true ) ) {
				// This could use str_replace() instead for compat with WP<6.2.
				$p = new WP_HTML_Tag_Processor( $tag );
				if ( $p->next_tag( 'script' ) ) {
					$p->set_attribute( $strategy, true );
					$tag = $p->get_updated_html();
				}
			}
			return $tag;
		},
		10,
		2
	);
}

But this approach could be combined with the wrapper functions, where the wrapper functions go ahead and set the strategy script data, and then the filter could be added once outside.

@adamsilverstein
Copy link
Author

Thanks for the feedback!

this approach could be combined with the wrapper functions, where the wrapper functions go ahead and set the strategy script data, and then the filter could be added once outside.

Right, that makes sense. I like how your approach only adds a single filter, that is much cleaner. I don't think I'll use WP_HTML_Tag_Processor though since it might not be available and the str_replace approach already feels reliable and performant.

I also like the wrapper functions because they maintain the same function signature, making implementation simple.

@westonruter
Copy link

@adamsilverstein Here's a hybrid of the two approaches: https://gist.github.com/westonruter/9694840a1cb940e66bdfb650e34c325e

Also I think the wrapper for wp_enqueue_script() can reduce a lot of code by simply calling wp_enqueue_script() after calling the wrapper for wp_register_script().

@adamsilverstein
Copy link
Author

@adamsilverstein Here's a hybrid of the two approaches: westonruter/9694840a1cb940e66bdfb650e34c325e

Great!

Also I think the wrapper for wp_enqueue_script() can reduce a lot of code by simply calling wp_enqueue_script() after calling the wrapper for wp_register_script().

Nice!

I'll update here based on that (except the WP_HTML_Tag_Processor bit so the code can work with older WP versions)

@westonruter
Copy link

@adamsilverstein I would just suggest making the replacement logic a bit more robust, for example:

$tag = str_replace( '<script ', '<script ' . $args['strategy'] . ' ', $tag );

This would prevent situations where the occurrence of ' src' somewhere in the string is mistakenly replaced, for example in this string:

<script src="/foo.js" class="foo-js src-local"></script>

@adamsilverstein
Copy link
Author

@adamsilverstein I would just suggest making the replacement logic a bit more robust, for example:

Good suggestion. Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment