Difference between async and defer in the script tag

The included scripts (JavaScript) block the loading of HTML code. When the browser (parser) reaches the <script> tag, it stops to load the file content and execute its code, and only then continues parsing HTML.

This behavior can slow down the display of HTML when many JavaScript files are being loaded on the page. Often the code of these files is not needed to display the HTML page. That's why it is recommended to include scripts at the end of the page. However, this recommendation cannot always be followed, and for such cases, there are other ways to not block the rendering of HTML.

The <script> element has two attributes, async and defer, which can give us more control over how and when files are loaded and executed.

Normal Execution

<html>
<head> ... </head>
<body>
	...
	<script src="script.js">
	....
</body>
</html>

Attribute async

It means that the script is completely independent:

  • The page does not wait for asynchronous scripts, the content is processed and displayed.
  • The DOMContentLoaded event and asynchronous scripts do not wait for each other:
    • DOMContentLoaded can occur either before the asynchronous script (if the asynchronous script finishes loading after the page is ready),
    • ...or after the asynchronous script (if it is short or already in the HTTP cache).
  • Other scripts do not wait for async, and scripts with async do not wait for other scripts.
<script async src="script.js">

Attribute defer

Instructs the browser to continue processing the page and load the script in the background, and then run this script when the DOM tree is fully built.

  • Scripts with defer never block the page.
  • Scripts with defer always run when the DOM tree is ready, but before the DOMContentLoaded event.
<script defer src="script.js">

Where and what to use?

It depends on the situation. Let's consider several questions on this topic.

Where is the <script> element located?

If the JavaScript file is located immediately before the closing </body> tag, using async or defer does not make sense because by this time the parser has already analyzed the entire HTML code.

Is the script self-contained?

For files (scripts) that are not needed for the operation of other scripts and do not have dependencies on other scripts, the async attribute is particularly useful. Since in this case it doesn't matter exactly when the script is executed, asynchronous loading is the most appropriate option.

Is a fully loaded DOM required for the script to work?

If necessary, using async is appropriate only if the script is designed for asynchronous loading - i.e. it waits for the DOM to load and only then starts its work.

Or you can use the defer attribute. In this case, the script call can be placed anywhere in the HTML.

Is the script small?

If the script is relatively small and depends on or is depended upon by other scripts, it can be embedded directly in the HTML (included inline).

Browser Support

Support for async - {percent}

Support for defer - {percent}

Adding the defer or async attributes in WordPress

Since WP 6.3

In WP 6.3: support for registering scripts with async and defer attributes has been added.

Example: Deferring script loading in the header

The loading strategy is specified by passing a key-value pair 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'
	]
);

Example: Asynchronously loading script in the footer

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

The same works for wp_enqueue_script().

Example: Using wp_script_add_data()

The loading strategy can also be specified using wp_script_add_data(). This can be useful when the current code needs to support WP versions less than 6.3.

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

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

Before WP 6.3

There are no built-in ways to do this, so we will use the script_loader_tag hook:

add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );
add_filter( 'script_loader_tag', 'my_script_loader_tag', 10, 2 );

function my_enqueue_scripts() {
	wp_enqueue_script( 'app', get_template_directory_uri() . 'js/app.js' );

	// Add async to the registered script.
	wp_script_add_data( 'app', 'async', true );
}

function my_script_loader_tag( $tag, $handle ) {

	foreach ( [ 'async', 'defer' ] as $attr ) {

		if ( ! wp_scripts()->get_data( $handle, $attr ) ) {
			continue;
		}

		// Prevent adding attribute twice.
		if ( ! preg_match( "~\s{$attr}[=>\s]~", $tag ) ) {
			$tag = preg_replace( '~(?=></script>)~', " $attr", $tag, 1 );
		}

		break; // Only async or defer, not both.
	}

	return $tag;
}

See example for more details.

Simplified version

add_action( 'wp_enqueue_scripts', 'my_scripts_method' );

function my_scripts_method(){

	// enqueue the script
	wp_enqueue_script( 'my-script', get_template_directory_uri() . '/js/my-script.js' );

	// Add the defer attribute to the script with id `my-script`
	add_filter( 'script_loader_tag', 'change_my_script', 10, 3 );

	function change_my_script( $tag, $handle, $src ){

		if( 'my-script' === $handle ){
			// return str_replace( ' src', ' async src', $tag );
			return str_replace( ' src', ' defer src', $tag );
		}

		return $tag;
	}
}