Add async or defer to script tags in WordPress

OVERVIEW:

WordPress provides many hooks into their API. These hooks are defined either as Actions or Filters. WordPress defines various hooks within API functions that allow programmers to intercept the output of a function and alter it before the API function returns that output data. This type of hook is called a Filter, for obvious reasons.

In this article, we will intercept the generation of <script> tags, and alter the output in an attempt to improve web site performance. The filter code will add either a defer or async attribute to the <script> tag.

The defer attribute delays the execution of a script until the web page has finished parsing. This example will add a defer attribute to scripts that exist on the same web site as the web page that is being rendered.

The async attribute begins execution of a script immediately in parallel with the rest of the web page being loaded and parsed. This example will add an async attribute to scripts that are loaded from any external web sites, such as google.com.

This code will also allow the programmer to define a list of exceptions. Any script with a filename that has a partial match in the exception list will be left unmodified (a filter hook function does not have to always modify the received data).

The follow code example can be placed in the current WordPress theme’s functions.php file for automatic execution.

A WordPress Lazy Script Loader

The Filter hook we will use is called ‘script_loader_tag’. This filter is called when WordPress is generating <script> tags. An example would be when the wp_enqueue_script( ) is called in a theme’s functions. The lazy_scripts( ) function is registered as a Filter hook callback function with WordPress in the call to add_filter( ).

Additional Notes:

  • The $domain variable is defined as static so that its value will be saved between calls. This is a minor performance improvement that ensures that for each request being processed, the function only has to calculate the web site’s domain name on the first call.
  • If the $url variable does not contain a sub-string of ‘.js’ the function returns without modifying the tag. There are some cases where the value of the src attribute might not be a file with a js extension, such as dynamically generated scripts, or script tags containing JSON-LD content.
  • If the $tag variable (the <script> tag generated by WordPress) already contains a sub-string of ‘ defer’ or ‘ async’ then the function returns without modifying the tag. There may be some plugins or themes that already specify one of these attributes when loading scripts.
  • If a script is locally embedded in the web page content, or the theme template has hard-coded <script> tags (instead of using wp_enqueue_script( ) calls) then this function will not be called for those <script> tags.
  • The add_filter( ) call has a priority parameter of PHP_INT_MAX. This is to try to execute this filter function after any other filters registered for this same hook.
  • You can try testing your web site with a free performance tester like gtmetrix.com with and without this filter enabled to see if your speed score improves. To do this, simply comment out the last line:
    // add_filter( ‘script_loader_tag’, ‘lazy_scripts’, PHP_INT_MAX, 3 );

/**
 * Improve performance by adding defer or async to script tag
 *
 * @param string $tag Script element tag
 * @param string $handle Script handle.
 * @param string $url Script URL.
 * @return string Modified script tag.
 */
function lazy_scripts( $tag, $handle, $url ) {
	/**
	 * List of exceptions to ignore
	 */
	static $exceptions = array(
		'/jquery.js',
		'/jsapi',
		'customize',  // WP Customizer scripts should not be deferred.
		'wp-',  // WP Customizer break if we don't eliminate this.
		'cloudflare.com'
		/* Add more exceptions here. */
	);

	/**
	 * Static var so that we look up site domain only once per request.
	 * @static
	 * @var string $domain
	 */
	static $domain = false;

	/**
	 * Ignore non-javascript.
	 */
	if ( ! strpos( $url, '.js' ) ) {
		return $tag;
	}

	/**
	 * Ignore exceptions.
	 */
	foreach ( $exceptions as $exception ) {
		if ( strpos( $url, $exception ) !== false ) {
			return $tag;
		}
	}

	/**
	 * Get the domain if we don't already have it.
	 */
	if ( false === $domain ) {
		$domain = parse_url( get_site_url(), PHP_URL_HOST );
	}

	/**
	 * Ignore script tags that already contain defer or async
	 */
	if ( strpos( $tag, ' defer' ) || strpos( $tag, ' async' ) ) {
		return $tag;
	}

	/**
	 * If script URL contains domain name, use defer
	 */
	if ( strpos( $url, $domain ) ) {
		return str_replace( '></script>', ' defer="defer"></script>', $tag );
	}

	/**
	 * If script is not in this domain, use async
	 */
	return str_replace( '></script>', ' async="async"></script>', $tag );
}

add_filter( 'script_loader_tag', 'lazy_scripts', PHP_INT_MAX, 3 );

Related posts: