Skip to content

Всё для WordPress, веб-разработки — и не только

⚡ Как добавить defer и async для скриптов WordPress в function.php

⚡ Как добавить defer и async для скриптов WordPress в function.php

Страницы грузятся медленно, Google PageSpeed Insights выдаёт оранжевые предупреждения, а клиент спрашивает: «почему сайт тормозит?» В девяти случаях из десяти корень проблемы, JavaScript, который блокирует рендеринг. Браузер доходит до <script>, останавливает построение DOM, грузит и выполняет скрипт, и только потом продолжает. На современном сайте с десятком плагинов эта задержка превращается в секунды.

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

Ниже, два рабочих метода: современный нативный (WP 6.3+) и проверенный фильтр script_loader_tag (WP 4.1+). Оба протестированы на реальных проектах, оба не ломают очередь зависимостей.

💡 Быстрый обзор:

  • Разберитесь, чем отличается defer от async и когда применять каждый, от этого зависит, не сломается ли функциональность после оптимизации
  • Используйте нативный метод WordPress 6.3+ через wp_enqueue_script() с параметром strategy, самый чистый способ, сохраняющий порядок выполнения
  • Если сайт на версии ниже 6.3, примените фильтр script_loader_tag с массивом хендлов, это работает начиная с WordPress 4.1
  • Для нескольких скриптов соберите массив хендлов и прогоните его циклом, один фильтр на все скрипты вместо копипасты

Что такое defer и async и когда их применять

Когда браузер встречает обычный тег <script>, он делает три вещи подряд: останавливает парсинг HTML, загружает скрипт, выполняет его. И только потом возвращается к HTML. На странице с пятью скриптами в <head> это означает, что пользователь видит белый экран, пока грузится последний плагин комментариев, даже если сам пост уже давно мог бы отрендериться.

Атрибуты defer и async решают эту проблему, но работают по-разному:

Атрибут

Когда загружается

Когда выполняется

Порядок выполнения

(нет)

Блокирует парсинг сразу

Немедленно после загрузки

По порядку в DOM

defer

Параллельно с парсингом

После полной загрузки DOM

По порядку в DOM

async

Параллельно с парсингом

Немедленно после загрузки

Кто первый загрузился

Defer, рабочий конь для большинства сценариев. Скрипт грузится параллельно с HTML, а выполняется только когда DOM полностью построен. Порядок сохраняется: скрипт A выполнится раньше скрипта Б, даже если Б загрузился быстрее. Это критично для jQuery и всего, что от него зависит.

Async, инструмент для независимых скрипов. Аналитика, реклама, виджеты соцсетей: им не нужен DOM, им не важен порядок, им нужно просто отработать как можно раньше. Но если поставить async на скрипт, который зависит от jQuery, получите $ is not defined с высокой вероятностью.

Простое правило: скрипт зависит от других скриптов или от DOM → defer. Скрипт полностью автономен → async. Сомневаетесь, всегда начинайте с defer.

Способ 1: Нативный метод WordPress 6.3+

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

Базовый синтаксис для defer:

1wp_enqueue_script(
2 'my-js-handle',
3 get_template_directory_uri() . '/js/my-script.js',
4 array('jquery'),
5 '1.0.0',
6 array(
7 'strategy' => 'defer',
8 'in_footer' => true,
9 )
10);

Для async, та же механика:

1wp_enqueue_script(
2 'google-analytics',
3 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX',
4 array(),
5 '1.0.0',
6 array(
7 'strategy' => 'async',
8 'in_footer' => false,
9 )
10);

Ключ in_footer внутри массива работает так же, как и старый булев параметр: true скрипт в футере, false в <head>. Для defer обычно ставят true (скрипт и так ждёт DOM, нет смысла грузить его рано), для async как удобно.

Главное преимущество нативного метода, ядро само проверяет дерево зависимостей. Если скрипт A с defer зависит от скрипта Б, а Б зарегистрирован без стратегии (блокирующим), WordPress не сломает вам сайт: он автоматически понизит стратегию скрипта A до блокирующей. При использовании script_loader_tag вы такой защиты лишены, фильтр тупо подставляет атрибут, не глядя на зависимости.

Важно: массив $args появился в WordPress 6.3. Если тема или плагин должны работать на версиях ниже, используйте способ 2 или добавьте проверку:

1if ( version_compare( $GLOBALS['wp_version'], '6.3', '>=' ) ) {
2 // нативный метод
3} else {
4 // фильтр script_loader_tag
5}

Способ 2: Фильтр script_loader_tag (WordPress 4.1+)

Если сайт работает на версии ниже 6.3 или нужно поддерживать обратную совместимость, применяйте проверенный фильтр script_loader_tag. Он существует с WordPress 4.1 и до сих пор работает безотказно.

Фильтр срабатывает прямо перед выводом тега <script> в HTML, вы получаете готовую строку тега, хендл скрипта и путь к файлу, и можете заменить src на defer="defer" src или async="async" src.

Одиночный скрипт с defer:

1function add_defer_to_my_script($tag, $handle) {
2 if ( 'my-js-handle' !== $handle ) {
3 return $tag;
4 }
5 return str_replace( ' src', ' defer="defer" src', $tag );
6}
7add_filter('script_loader_tag', 'add_defer_to_my_script', 10, 2);

Код размещается в functions.php активной темы или, что правильнее, в отдельном плагине для сниппетов типа Code Snippets или WPCode. Если положить в functions.php дочерней темы, при смене темы скрипты снова станут блокирующими, и вы об этом узнаете не сразу.

Хендл скрипта, это первый параметр, который вы передали в wp_register_script() или wp_enqueue_script(). Именно он фигурирует в условии if. Не угадывайте хендл, откройте исходный код плагина или темы и найдите вызов wp_enqueue_script.

Defer и async для нескольких скриптов

Добавлять по одному фильтру на каждый скрипт, путь к пухлому functions.php и ошибкам при копипасте. Правильное решение: массив хендлов и один фильтр с циклом.

1function add_defer_to_scripts($tag, $handle) {
2 $scripts_to_defer = array(
3 'my-js-handle',
4 'another-handle',
5 'third-party-lib',
6 );
7
8 foreach ( $scripts_to_defer as $defer_script ) {
9 if ( $defer_script === $handle ) {
10 return str_replace( ' src', ' defer="defer" src', $tag );
11 }
12 }
13 return $tag;
14}
15add_filter('script_loader_tag', 'add_defer_to_scripts', 10, 2);

Для async, меняется только атрибут и название массива:

1function add_async_to_scripts($tag, $handle) {
2 $scripts_to_async = array(
3 'google-tag-manager',
4 'facebook-pixel',
5 'hotjar',
6 );
7
8 foreach ( $scripts_to_async as $async_script ) {
9 if ( $async_script === $handle ) {
10 return str_replace( ' src', ' async="async" src', $tag );
11 }
12 }
13 return $tag;
14}
15add_filter('script_loader_tag', 'add_async_to_scripts', 10, 2);

Оба фильтра можно вешать одновременно, defer на свои скрипты, async на сторонние трекеры. Они работают независимо и не конфликтуют.

Практический пример: Google Maps API

Google Maps, классический кандидат на defer. Карта обычно в футере страницы контактов, скрипт тянет 100+ КБ, а пользователю карта нужна далеко не сразу. При этом сам API не зависит от других скриптов страницы, идеальный случай.

Подключаем и откладываем:

1// functions.php темы
2function enqueue_google_maps() {
3 if ( ! is_page('contacts') ) {
4 return;
5 }
6
7 wp_enqueue_script(
8 'google-maps-api',
9 'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY',
10 array(),
11 null,
12 array(
13 'strategy' => 'defer',
14 'in_footer' => true,
15 )
16 );
17}
18add_action('wp_enqueue_scripts', 'enqueue_google_maps');

Тот же результат через script_loader_tag:

1function add_defer_to_google_maps($tag, $handle) {
2 if ( 'google-maps-api' !== $handle ) {
3 return $tag;
4 }
5 return str_replace( ' src', ' defer="defer" src', $tag );
6}
7add_filter('script_loader_tag', 'add_defer_to_google_maps', 10, 2);

После установки любого из вариантов, обязательно проверьте карту на странице контактов. Откройте консоль браузера (F12), убедитесь, что нет ошибок JavaScript, и что карта отрисовалась корректно. Если появилась ошибка вроде initMap is not a function, значит, ваш инициализирующий скрипт тоже нужно пометить как defer и разместить строго после подключения API.

Как проверить, что defer и async работают

После внедрения, проверка. Без неё вы не знаете, сработала оптимизация или просто лежит мёртвым кодом.

Откройте исходный код страницы (Ctrl+U) и найдите свои скрипты. В теге <script> должны появиться атрибуты:

1<script defer="defer" src="/wp-content/themes/my-theme/js/my-script.js"></script>

Если атрибутов нет, проверьте, совпадает ли хендл в фильтре с реальным хендлом скрипта. Частая ошибка: в wp_enqueue_script хендл my-plugin-frontend, а в фильтре my_plugin_frontend. Дефис против подчёркивания, и фильтр молча пропускает скрипт.

Финальный штрих, Google PageSpeed Insights или Lighthouse во вкладке Audits инструментов разработчика. Раздел «Устраните ресурсы, блокирующие отображение» должен показать улучшение. Конкретный выигрыш зависит от количества и размера скриптов, но для типового сайта на WordPress с 5-7 плагинами сокращение блокирующего JavaScript на 40-60%, достижимый результат.

⁉️🤔 Частые вопросы

Можно ли использовать и defer, и async на одном скрипте?

Нет. Если указать оба атрибута одновременно, браузер проигнорирует defer и отработает скрипт как async. Это поведение зашито в спецификации HTML, async всегда приоритетнее. Выберите что-то одно, исходя из того, важен ли порядок выполнения.

Что делать, если после добавления defer скрипт перестал работать?

Скорее всего, скрипт ожидает, что DOM ещё не построен, и пытается манипулировать элементами, которых в момент выполнения нет. Замените defer на стандартную блокирующую загрузку для этого конкретного скрипта. Или оберните код скрипта в DOMContentLoaded, тогда он сможет работать с defer без ошибок. Второй вариант предпочтительнее: вы сохраняете оптимизацию и чините совместимость.

В чём разница между defer и переносом скрипта в футер через wp_enqueue_script с $in_footer = true?

$in_footer = true всего лишь перемещает тег <script> из <head> в конец <body>. Скрипт по-прежнему блокирует рендеринг, просто позже. defer загружается параллельно с парсингом HTML и выполняется строго после построения DOM. Совместное использование (in_footer => true + strategy => 'defer') даёт максимальный эффект: скрипт в футере не задерживает первый рендер, а defer гарантирует, что он не заблокирует и финальную отрисовку.

Нужно ли обновлять WordPress до 6.3 ради нативного метода?

Если сайт на версии 6.2 или старше, обновляться стоит не только ради strategy. WordPress 6.3 закрыл десятки уязвимостей и принёс улучшения производительности самого ядра. Но если обновление по каким-то причинам невозможно, фильтр script_loader_tag работает абсолютно надёжно с версии 4.1, выпущенной в 2014 году. Вы ничего не теряете, используя его.

Как быть с jQuery, defer или оставить как есть?

jQuery должен загружаться с defer, если все зависимые от него скрипты тоже помечены defer. Проблема в том, что WordPress-плагины крайне редко управляют атрибутами своих скриптов. Если вы ставите defer на jQuery, а плагин контактной формы подключает свой скрипт без атрибутов, браузер выполнит плагин раньше jQuery, и форма сломается. Практический совет: начните с defer для собственных скриптов темы. jQuery не трогайте, пока не протестируете каждый плагин на сайте.

Что ставить на боевой сайт в 2026 году

Если сервер работает на WordPress 6.3 или новее, только нативный метод. Чистый код, защита от конфликтов зависимостей, поддержка ядром. Начинайте с defer для всех скриптов темы и критически важных плагинов; async резервируйте для аналитики и сторонних виджетов.

Если версия ниже 6.3, фильтр script_loader_tag с массивом хендлов. Работает десятилетие, ломаться нечему. Единственное, чего он не умеет, автоматически проверять дерево зависимостей, поэтому добавляйте скрипты в массив по одному и после каждого проверяйте сайт.

И главное: ни один метод не заменит ревизию самих скриптов. Если плагин галереи подключает 15 файлов ради показа трёх картинок, ни defer, ни async радикально не спасут. Оптимизация загрузки начинается с вопроса «а нужен ли этот скрипт вообще», и только потом, «как его загрузить».

🔗 Официальная документация WordPress 6.3, Script Loading Strategies