Разница между async и defer у тега script

Подключаемые скрипты (JavaScript) блокирует загрузку HTML кода. Когда браузер (парсер) доходит до тега <script> он останавливается, чтобы загрузить контент файла и выполнить его код, и только после этого продолжает парсинг HTML.

Такое поведение может тормозить отображение HTML, когда на странице загружаются много файлов JavaScript. Часто код этих файлов не нужен, чтобы показать HTML страницы. Именно поэтому рекомендуется подключать скрипты в конце страницы. Однако эту рекомендацию не всегда можно соблюсти и для таких случаев есть другие способы не блокировать отрисовку HTML.

У элемента <script> есть два атрибута, async и defer, которые могут дать нам больше контроля над тем, как и когда файл загружаются и выполняются.

Обычное выполнение

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

Атрибут async

Означает, что скрипт абсолютно независим:

  • Страница не ждёт асинхронных скриптов, содержимое обрабатывается и отображается.
  • Событие DOMContentLoaded и асинхронные скрипты не ждут друг друга:
    • DOMContentLoaded может произойти как до асинхронного скрипта (если асинхронный скрипт завершит загрузку после того, как страница будет готова),
    • …так и после асинхронного скрипта (если он короткий или уже содержится в HTTP-кеше).
  • Остальные скрипты не ждут async, и скрипты c async не ждут другие скрипты.
<script async src="script.js">

Атрибут defer

Cообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM дерево будет полностью построено.

  • Скрипты с defer никогда не блокируют страницу.
  • Скрипты с defer всегда выполняются, когда дерево DOM готово, но до события DOMContentLoaded.
<script defer src="script.js">

Где и что использовать?

Зависит от ситуации. Рассмотрим несколько вопросов на эту тему.

Где находится элемент <script>?

Если файл JavaScript находится непосредственно перед закрывающим тегом </body>, использовать async или defer не имеет смысла поскольку к этому моменту парсер уже проанализировал весь HTML код.

Является ли скрипт самодостаточным?

Для файлов (скриптов), которые не нужны для работы других скриптов и сами не имеют зависимостей от других скриптов, атрибут async особенно полезен. Поскольку в этом случае все равно, в какой именно момент выполняется скрипт, асинхронная загрузка является наиболее подходящим вариантом.

Нужен ли полностью загруженный DOM для работы скрипта?

Если это необходимо, то использование async уместно только в случае если скрипт рассчитан на асинхронную загрузку - т.е. он ждет события пока загрузиться DOM и только потом начинает свою работу.

Или можно использовать атрибут defer. В этом случае размещать вызов script можно в любом месте HTML.

Маленький ли скрипт?

Если скрипт относительно мал и от него зависят или он зависит от других скриптов, то его можно встроить прямо в HTML (подключить inline).

Поддержка браузерами

Поддержка async - {percent}

Поддержка defer - {percent}

Добавление атрибутов defer или async в WordPress

С версии WP 6.3

В WP 6.3: появилась поддержка регистрации скриптов с атрибутами async и defer.

Пример: defer подключение скрипта в шапке

Стратегия загрузки указывается путем передачи пары ключ-значение стратегии в параметр $args (бывший $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',
	]
);

Тоже самое работает и для wp_enqueue_script().

Пример: Использование wp_script_add_data()

Стратегию загрузки также можно указать с помощью wp_script_add_data(). Это может быть полезно, когда текущий код должен поддерживать версии WP меньше 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' );

До WP 6.3

Штатных способов сделать это нет, поэтому будем пользоваться хуком script_loader_tag:

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' );

	// Добавляем async к зарегистрированному скрипту.
	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;
}

Подробнее смотрите пример.

Упрощенный вариант

add_action( 'wp_enqueue_scripts', 'my_scripts_method' );

function my_scripts_method(){

	// подключаем скрипт
	wp_enqueue_script( 'my-script', get_template_directory_uri() . '/js/my-script.js' );

	// Добавляем атрибут defer к скрипту с 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;
	}
}