WP 6.3: async и defer аттрибуты при регистрации скриптов

Click here to view original web page at wp-kama.ru

В WordPress 6.3 появилась поддержка регистрации скриптов с атрибутами async и defer в рамках улучшения существующего API Scripts. Решается проблема давнего тикета путем добавления стратегии загрузки скриптов.

Поддерживаются следующие стратегии:

  • Блокирование - по умолчанию. Скрипт блокирует загрузку страницы.
  • Отложенная - стратегия defer.
  • Асинхронная - стратегия async.

Оглавление

Зачем это нужно?

Добавление defer или async к тегам скриптов позволяет загружать скрипты, не "блокируя" остальную часть загрузки страницы, что приводит к повышению производительности сайтов за счет улучшения работы Largest Contentful Paint (LCP). Подробнее читайте тут.

До появления этого усовершенствования разработчикам приходилось прибегать к не самым лучшим альтернативам, таким как:

  • непосредственная фильтрация тегов на этапе вывода с помощью фильтра script_loader_tag.
  • или, что еще хуже, фильтра clean_url.
  • или непосредственная обработка вывода тегов с помощью wp_print_script_tag и фильтра wp_script_attributes.

Все эти подходы считаются "плохими", потому что не учитывает зависимостей скриптов, которые могут привести к проблемам совместимости или ошибкам при работе с другими скриптами.

Разницу между defer и async читайте в статье: Разница между async и defer у тега script.

Если коротко то она такая:

  • Отложенные скрипты. Скрипты с атрибутом defer, выполняются только после полной загрузки дерева DOM (но до событий DOMContentLoaded). Отложенные скрипты, в отличие от асинхронных, выполняются в том же порядке, в котором они были добавлены в DOM.

  • Асинхронные скрипты. Скрипты с атрибутом async, выполняются сразу же после их загрузки браузером и не блокируют загрузку (браузер не ждет загрузки скрипта). Асинхронные скрипты выполняются не по порядку. Так, например, скрипт B (который в коде идет после скрипта A) может быть выполнен первым, так как браузер загрузил его раньше чем скрипт A. Такие скрипты могут выполняться как до полного построения DOM, так и после события DOMContentLoaded.

Краткое описание изменений

В общих чертах изменения можно описать следующим образом:

  • В WordPress добавлена поддержка указания стратегии загрузки скриптов для функций wp_register_script() и wp_enqueue_script().

  • Эти функции имеют новые сигнатуры - изменился тип параметра $in_footer - теперь вместо bool типа используется массив в котором можно указать где и как следует загружать скрипт. При этом сохранена обратная совместимость с bool типом.

  • Стратегия загрузки может также быть указана и обратно совместимым способом через wp_script_add_data().

  • В класс WP_Scripts{} были внесены дополнения и усовершенствования для возможности указать стратегию загрузки скрипта.

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

Стратегия загрузки указывается путем передачи пары ключ-значение стратегии в параметр $args (бывший $in_footer).

wp_register_script(
	'foo',
	'/path/to/foo.js',
	[],
	'1.0.0',
	[
		'strategy' => 'defer'
	]
);

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

wp_register_script(
	'bar',
	'/path/to/bar.js',
	[],
	'1.0.0',
	[
		'in_footer' => true,
		'strategy'  => 'async',
	]
)

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

Подробности внедрения

Данная функция учитывает дерево зависимостей скрипта (его зависимости и/или зависимые компоненты) при выборе "подходящей стратегии", чтобы не допустить применения стратегии, подходящей для одного скрипта, но вредящей другим в дереве, вызывая непреднамеренное нарушение порядка выполнения. Этого практически невозможно добиться прежними способами добавления атрибутов стратегии загрузки скриптов при использовании альтернативных "плохих" способов, описанных выше.

Техническая реализация загрузки скриптов была выполнена без нарушения уже имеющегося API скриптов. Были усовершенствованы все функции API скриптов, такие как: wp_register_script() и wp_enqueue_script().

О зависимостях

Чтобы избежать путаницы, проясним разницу между зависимостями скрипта и зависимыми скриптами:

  • зависимые - это скрипты, которые зависят от данного скрипта, т.е. скрипты, которые автоматически подключают данный скрипт перед своим подключением, так как они от него зависят.

  • зависимости - это наоборот - скрипты, от которых зависит текущий скрипт, т.е. они должны быть вызваны до того, как будет вызван скрипт.

Изменения параметра $in_footer функций wp_register_script() и wp_enqueue_script().

Наиболее заметным изменением существующих функций wp_register_script() и wp_enqueue_script() является изменение сигнатуры функции, в которой параметр $in_footer (ранее являвшийся булевым параметром) был перегружен и стал принимать массив $args с любым из следующих ключей:

in_footer (bool)
Ведет себя так же, как и предыдущая реализация параметра $in_footer.
strategy (string)

Указывает как (по какой стратегии) должен загружаться скрипт. На данный момент доступно два значения: defer и async для отложенных и асинхронных скриптов соответственно.

По умолчанию используется блокирующее поведение, что сохраняет обратную совместимость с существующими регистрациями и вызовами скриптов.

Сохранение обратной совместимости

Для предыдущего/существующего использования функций wp_register_script() и wp_enqueue_script(), использующих булевый параметр $in_footer, сохраняется полная обратная совместимость. Поэтому это улучшение не ломающее API.

Несмотря на то, что изменения не являются критичными, при использовании нового параметра $args в плагине/теме/кодебазе, работающей на WordPress < 6.3, существует один сценарий, при котором $in_footer будет неправильно понят ядром.

Возьмем, к примеру, следующий сценарий:

wp_register_script(
	'foo',
	'/path/to/foo.js',
	[],
	'1.0.0',
	[
		'strategy'  => 'defer',
		'in_footer' => false, // Note: This is the default value.
	]
);

В WordPress >= 6.3 это будет работать по новой логике. Скрипт будет подключен в шапке с defer атрибутом.

Однако в версиях WordPress < 6.3 такой массив для $in_footer, будет расцениваться как булево значение true, и скрипт будет подключен в подвале, хотя разработчик указал что скрипт нужно подключить в шапке. Однако, можно утверждать, что в версиях WordPress, не поддерживающих отложенные/асинхронные скрипты, вывод скриптов в подвале - это даже лучше чем вывод его в шапке.

Самый простой способ предотвратить такую проблему совместимости - передать стратегию не через параметр $args, а через функцию wp_script_add_data():

wp_register_script(
	'foo',
	'/path/to/foo.js',
	[],
	'1.0.0',
	false
);

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

Другой вариант, когда кроме прочего нужно чтобы обрабатывались зависимости - это создать свою функцию-обертку для регистрации скриптов, которая будет учитывать изменения версии WordPress 6.3.

Пример такой обертки для регистрации/очередей скриптов может выглядеть следующим образом:

myplugin_register_script( $handle, $src, $deps, $ver, $args ) {
	global $wp_version;

	if ( version_compare( $wp_version,'6.3', '>=' ) ) {
		wp_register_script( $handle, $src, $deps, $ver, $args );
	} else {
		$in_footer = isset( $args['in_footer'] ) ? $args['in_footer'] : false;

		wp_register_script( $handle, $src, $deps, $ver, $in_footer );
	}
}

Изменение указанной стратегии из-за зависимостей

Несмотря на то, что разработчик указал конкретную стратегию загрузки, итоговая стратегия может отличаться от указанной. Это зависит от зависимостей скрипта и inline скриптов.

Например, если разработчик регистрирует скрипт foo со стратегией defer, то для сохранения намеченного порядка подключения зависимости скрипта должны использовать стратегию defer или стратегию блокировки, а зависимые скрипты - стратегию defer. Если зависимый от foo скрипт будет зарегистрирован со стратегией блокирования, то скрипт foo и все его зависимости автоматически станут блокирующими, несмотря на то что была указана стратегия отсрочки (defer).

Новая логика в классе WP_Scripts отвечает за выполнение ряда логических проверок, которые гарантируют, что конечная стратегия для данного скрипта является наиболее подходящей, исходя из вышеописанных факторов.

Хендлер не унаследует стратегию, которая является "более строгой", чем предполагалось, т.е. скрипт, помеченный для отложенной загрузки, никогда не будет изменен на асинхронный, но обратный результат допустим, если этого требуют внешние факторы.

inline скрипты

Смотрите: wp_add_inline_script()

Существуют некоторые нюансы при применении стратегии загрузки для скриптов которые зависят от Инлайн-скриптов, так как это влияет на конечный результат применения выбранной стратегии.

Инлайн-скрипты зарегистрированные в позиции before, почти всегда неизменны в своем поведении, потому что они будут подключены до того, как основной/родительский скрипт будет обрабатываться любой из стратегий: блокирующая, defer или async.

Однако инлайн-скрипты, зарегистрированные в позиции after (по умолчанию для wp_add_inline_script(), будут влиять на конечную стратегию загрузки основного/родительского скрипта, если основной/родительский скрипт имеет async или defer стратегию выполнения.

Таким образом, если инлайн-скрипт подключается в позиции after, то базовый/основной скрипт будет считаться блокирующим - указанная для него стратегия "defer" или "async", будут отменена и будет использоваться "блокирующая" стратегия. Это, в свою очередь, может повлиять на дерево зависимостей базового скрипта, и все скрипты в нём тоже могут быть расценены как блокирующие.

Для продолжения обсуждений на эту тему и возможных изменений был открыт дополнительный тикет #58632.

Переход на новый API

Реализации кода, использующие устаревшие методы добавления атрибутов async или defer к тегам скриптов, должны перейти на новый API. К ним относятся скрипты, в которых атрибуты добавлялись в тег скрипта через фильтр script_loader_tag или, что еще хуже, через фильтр clean_url.

В следующих примерах показана возможная реализация через фильтры script_loader_tag и clean_url. Такие реализации обязательно нужно перевести на новый API:

Пример 1: Добавление атрибута defer через script_loader_tag

add_filter( 'script_loader_tag', 'old_approach', 10, 2 );

function old_approach( $tag, $handle ) {

	if ( 'foo' !== $handle ) {
		return $url;
	}

	return str_replace( ' src=', ' defer src=', $tag );
}

Пример 1: Добавление атрибута defer через clean_url

add_filter( 'clean_url', 'old_brittle_approach' );

function old_brittle_approach( $url ) {

	if ( false === strpos( $url, 'foo.js' ) ) {
		return $url;
	}

	return "$url' defer ";
}

Если для добавления в скрипт атрибутов defer или async вы используете подход, аналогичный описанному выше, перейдите на новый API, используя любой из подходов, описанных ранее в этом сообщении.

--

Источник: https://make.wordpress.org/core/2023/07/14/registering-scripts-with-async-and-defer-attributes-in-wordpress-6-3/