Отложенная загрузка скриптов: defer, async и что реально ускоряет
Чем отличаются defer и async, какие скрипты можно грузить лениво, как отложить виджеты и счётчики и не сломать формы.
Содержание
- Почему один тег script задерживает всю страницу
- defer против async: разница в порядке выполнения
- Что откладывать нельзя
- Ленивая загрузка тяжёлых виджетов
- Счётчики и пиксели: где отложенная загрузка съедает данные
- Что показывают замеры до и после
- Как это сделать в WordPress без плагина
- Когда плагин всё-таки удобнее
- Чек-лист перед выкаткой
- Что делать дальше
- Частые вопросы
Атрибут defer откладывает выполнение до разбора страницы и сохраняет порядок скриптов, async выполняет сразу по загрузке и порядок ломает. Отсюда правило: библиотекам и зависимому коду нужен defer, независимым счётчикам подходит async.
Отложенная загрузка скриптов это первое, что советуют в любом отчёте PageSpeed, и первое, чем ломают сайты владельцы, которые поставили галочку в плагине оптимизации. Страница ускорилась на 20 баллов, зато форма заявки перестала отправляться, а слайдер на главной прыгает при загрузке. Разберём, чем отличаются defer и async, какие скрипты трогать нельзя, как отложить чат и счётчики без потери данных, и что показывают замеры до и после.

Я вожусь с этим на клиентских сайтах шесть лет. Больше половины случаев «сайт после оптимизации сломался» это именно неаккуратно отложенные скрипты. Поэтому дальше будет много занудства про порядок выполнения: без него отложенная загрузка превращается в лотерею.
Почему один тег script задерживает всю страницу
Браузер читает HTML сверху вниз и строит DOM. Когда парсер встречает обычный <script src="..."> без атрибутов, он останавливается. Полностью. Не строит DOM дальше, не рисует то, что уже разобрал, ждёт: скачать файл, распарсить, выполнить. Только потом продолжает.
Это поведение из девяностых, и оно логично: скрипт мог вызвать document.write() и дописать HTML прямо в текущую точку. Браузер не знает заранее, что там внутри, поэтому обязан подождать.
Дальше простая арифметика. Файл на 80 КБ с российского хостинга по мобильному 4G это примерно 300-400 мс на скачивание плюс 60-150 мс на парсинг и выполнение на среднем Android. Три таких файла в <head> и вы отдали полторы секунды до первой отрисовки, даже если картинки и текст давно приехали.
Отдельно про jQuery. На типовом сайте малого бизнеса до сих пор живёт связка jquery.min.js плюс jquery-migrate.min.js плюс десяток плагинов, повешенных на $(document).ready(). Это 90-120 КБ блокирующего кода, который нужен ради двух вещей: аккордеона в FAQ и всплывающего меню на мобильном.
Проверить, кто именно блокирует, можно без всяких сервисов. Откройте вкладку Network в DevTools, включите фильтр JS и колонку «Initiator», отсортируйте по времени старта. Всё, что стартует до события DOMContentLoaded и не имеет атрибутов, и есть ваши блокираторы. Второй способ, если доступа к браузеру нет:
curl -s https://example.ru/ | grep -oP '<script[^>]*>' | grep -v 'defer|async|type="application/ld'
Команда выдаёт список тегов без отложенных атрибутов. На запущенном WordPress их обычно бывает от четырёх до пятнадцати.
defer против async: разница в порядке выполнения
Оба атрибута снимают блокировку парсера: файл качается параллельно с разбором HTML. Разница в том, когда он выполняется и в каком порядке относительно соседей.
async выполняет скрипт сразу, как только тот докачался. Парсер в этот момент прерывается. Порядок между несколькими async-скриптами не определён вообще: кто первым скачался, тот первым и стартовал. Файл на 5 КБ обгонит файл на 100 КБ, даже если в HTML стоит после него.
defer откладывает выполнение до момента, когда весь HTML разобран, но до события DOMContentLoaded. Порядок между defer-скриптами сохраняется тот, в котором они стоят в разметке. Это критично: если slider.js рассчитывает на уже загруженный jquery.js, только defer гарантирует, что jQuery выполнится первым.
| Свойство | Обычный script | defer | async | type=»module» |
|---|---|---|---|---|
| Блокирует парсер при скачивании | да | нет | нет | нет |
| Когда выполняется | сразу на месте | после парсинга HTML | сразу после скачивания | после парсинга (как defer) |
| Порядок относительно других | по разметке | по разметке | случайный | по разметке |
| Видит весь DOM | нет | да | не гарантированно | да |
| Работает с document.write | да | ломается | ломается | ломается |
| Куда годится | почти никуда | код темы, jQuery, UI | независимые счётчики | современные сборки |
Практическое правило, к которому я пришёл: defer по умолчанию для всего своего кода, async только для полностью изолированных счётчиков, которые ни от чего не зависят и от которых не зависит ничего.
Ещё одна деталь, о которой забывают. Оба атрибута работают только на внешних скриптах с src. Инлайновый <script>alert(1)</script> с атрибутом defer выполнится немедленно, атрибут просто проигнорируется. Если у вас в шапке лежит инлайновая инициализация чего-нибудь на 30 строк, её сначала надо вынести в файл, и только потом откладывать.
И третье: defer-скрипт выполняется до DOMContentLoaded, а значит обработчики внутри вроде document.addEventListener('DOMContentLoaded', ...) продолжат работать нормально. А вот код, который вешается на window.onload внутри async-скрипта, может опоздать и не сработать, если onload уже отстрелялся.
Что откладывать нельзя
Есть категория скриптов, где отложенная загрузка ломает функциональность молча, без ошибок в консоли. Их надо знать в лицо.
Инлайновые зависимости. Классика WordPress: тема ставит <script src="main.js">, а сразу за ним инлайновый блок <script>MainApp.init({apiKey: "..."})</script>. Отложили внешний файл, инлайновый остался немедленным, MainApp на момент вызова не существует, консоль пишет «MainApp is not defined». Лечится либо переносом инициализации внутрь основного файла, либо оборачиванием в слушатель DOMContentLoaded.
Скрипты, которые правят вёрстку до отрисовки. Переключатель тёмной темы, скрипт скрытия элементов по A/B тесту, класс js-enabled на <html>. Отложите такой код и получите мигание: страница нарисовалась в одном виде, через 200 мс перекрасилась. Это ещё и портит CLS, за который Яндекс и Google снижают оценку.
Полифилы и загрузчики. Если в проекте есть RequireJS, SystemJS или сборка с динамическими импортами, лоадер обязан быть готов раньше остального. Тут порядок важнее скорости.
Платёжные и антифрод-скрипты. Виджеты ЮKassa, Тинькофф, СБП-кнопки, а также скрипты сбора отпечатка устройства. Они часто требуют исполнения в конкретный момент, а сломанная оплата стоит дороже, чем полсекунды LCP.
Скрипты, использующие document.write. Старые рекламные сети и партнёрские виджеты. С defer или async браузер просто выбросит запись в консоль («A parser-blocking script was invoked via document.write») и проигнорирует вставку.
Как понять до продакшена, что именно сломается. Порядок, который я использую на клиентских сайтах:
- Разворачиваем копию на поддомене или в staging, откладываем всё подряд, кроме платёжки.
- Прогоняем сценарии руками: отправка формы, открытие модалки, слайдер, фильтр каталога, мобильное меню, корзина.
- Смотрим консоль на каждой ключевой странице, не только на главной.
- Возвращаем немедленную загрузку тем скриптам, которые упали, и ищем причину точечно.
- Только после этого катим на боевой.
Один прогон занимает 40-60 минут на среднем сайте. Это дешевле, чем неделю не замечать, что заявки перестали приходить. И да, проверять надо руками, а не по скриншоту главной: половина поломок вылезает на внутренних страницах.
Ленивая загрузка тяжёлых виджетов
Отдельная история это сторонние виджеты: онлайн-чат, обратный звонок, карта, отзывы, календарь записи. Тут одного defer мало. Виджет чата тянет за собой 300-600 КБ ресурсов, открывает WebSocket и обычно грузит собственные шрифты. Даже отложенный, он съест процессор на мобильном ровно в тот момент, когда пользователь пытается скроллить.
Правильный подход это загрузка по первому взаимодействию. Виджет не подключается вообще, пока пользователь не проявил активность: скролл, движение мыши, касание экрана, нажатие клавиши. Плюс страховочный таймер и явная кнопка.
(function () {
var loaded = false;
function loadChat() {
if (loaded) return;
loaded = true;
var s = document.createElement('script');
s.src = 'https://code.jivo.ru/widget/YOUR_ID';
s.async = true;
document.body.appendChild(s);
}
['scroll', 'mousemove', 'touchstart', 'keydown'].forEach(function (evt) {
window.addEventListener(evt, loadChat, { once: true, passive: true });
});
document.querySelectorAll('[data-open-chat]').forEach(function (el) {
el.addEventListener('click', function (e) {
e.preventDefault();
loadChat();
});
});
setTimeout(loadChat, 10000);
})();
Что здесь важно. Флаг loaded защищает от двойной вставки, если пользователь одновременно скроллит и двигает мышь. Опция once: true снимает слушатель после первого срабатывания. passive: true говорит браузеру, что обработчик не будет вызывать preventDefault, и скролл не подтормаживает. Таймер на 10 секунд нужен для тех, кто открыл страницу и читает не шевелясь: чат всё-таки появится.
Такая схема на клиентском сайте с Jivo убрала 480 КБ из начальной загрузки и подняла мобильный PageSpeed с 41 до 68 баллов. Конверсия в обращения через чат при этом не просела: люди, которым нужен чат, всё равно двигают мышь к иконке. Подробный разбор того, сколько именно весят популярные виджеты, есть в статье про виджеты Jivosite и Битрикс24 и скорость.
Карта Яндекса откладывается похожим образом, только по появлению блока в зоне видимости. Вместо интерактивной карты в разметке лежит статичная картинка с координатами, а настоящий API подгружается, когда пользователь до неё доскроллил, через IntersectionObserver. На странице контактов это экономит порядка 700 КБ для тех, кто до карты не дошёл, а таких обычно две трети.
Счётчики и пиксели: где отложенная загрузка съедает данные
Тут начинается зона, где техническая правильность конфликтует с бизнес-задачей, и решать должен владелец, а не оптимизатор.
Яндекс Метрика официально ставится с атрибутом async и своей очередью команд. Отложить её на условный setTimeout(3000) технически можно, но вы потеряете часть визитов: люди, закрывшие страницу за первые три секунды, в статистику не попадут. По моим наблюдениям на сайтах услуг это 8-15 процентов трафика. Хуже того, у вас поедет показатель отказов и время на сайте, потому что короткие визиты просто исчезнут из выборки.
Мой вывод простой: Метрику не откладывать дальше, чем async. Она сама по себе не так тяжела, как принято думать, а вот испорченная аналитика лечится месяцами. Отдельно я разбирал, что именно в ней тормозит и что можно отключить, в статье про Метрику, которая замедляет сайт.
Разложу по типам, что можно и чего нельзя:
| Скрипт | Что делать | Риск при отложении |
|---|---|---|
| Яндекс Метрика (базовый счётчик) | async, как в официальном коде | потеря коротких визитов |
| Вебвизор и карта кликов | отложить на interaction | почти нет, запись начнётся позже |
| Пиксель ВК, myTarget | отложить на 2-3 секунды | небольшая потеря охвата ретаргетинга |
| Коллтрекинг с подменой номера | не откладывать | покажется старый номер, звонок не атрибутируется |
| Сквозная аналитика (Roistat, Calltouch) | не откладывать | рвётся связка визит-заявка |
| Виджеты соцсетей и лайков | грузить по клику | нет |
| A/B тесты и персонализация | не откладывать | мигание контента, испорченный тест |
Отдельно про коллтрекинг. Скрипт подмены номера должен отработать до того, как пользователь увидит телефон в шапке. Отложили его, человек увидел статичный номер, позвонил по нему, и рекламная кампания получила ноль звонков в отчёте. Такое я разбирал у клиента дважды, и оба раза причиной был плагин оптимизации с включённой опцией «отложить весь JavaScript».
Что показывают замеры до и после
Цифры имеют смысл только с конкретной методикой, поэтому опишу свою. Замер делается через Lighthouse в режиме мобильной эмуляции с троттлингом 4G, по три прогона, берётся медиана. Локальный запуск честнее, чем публичный PageSpeed, потому что там сеть до сервера с российского хостинга измеряется криво.
npx lighthouse https://example.ru/
--only-categories=performance
--form-factor=mobile --throttling-method=simulate
--output=json --output-path=./before.json
Реальный пример с сайта услуг на WordPress, тема с jQuery, слайдер, чат Jivo, Метрика с Вебвизором, пиксель ВК. Хостинг shared, PHP 8.2, кэш на LiteSpeed.
| Метрика | До | После | Что дало эффект |
|---|---|---|---|
| Performance (mobile) | 38 | 71 | суммарно |
| LCP | 4,9 с | 2,3 с | defer на jQuery и коде темы |
| FCP | 2,7 с | 1,4 с | снятие блокировки в head |
| TBT | 1 240 мс | 310 мс | ленивый чат и Вебвизор |
| INP (полевые данные, месяц) | 340 мс | 180 мс | меньше работы в основном потоке |
| Вес JS до первой отрисовки | 612 КБ | 94 КБ | ленивые виджеты |
Обратите внимание на распределение вкладов. Атрибут defer на своих файлах дал основную часть улучшения LCP и FCP, потому что снял блокировку рендеринга. А вот TBT и INP просели вниз в основном за счёт ленивых виджетов: они не блокировали отрисовку, но сжигали процессор мобильника и держали основной поток занятым, из-за чего страница не отвечала на касания.
Именно поэтому одного defer часто мало. Отложенный скрипт всё равно выполняется, просто позже. Если он тяжёлый, он ударит по отзывчивости, а INP считается по худшему взаимодействию за сессию. Что изменилось в этой метрике и почему старый FID больше не показатель, я расписывал в статье про INP вместо FID.
Чего ждать не стоит. Отложенная загрузка почти не влияет на TTFB: там виноват сервер, а не браузер. И она не спасёт сайт, где главная весит 8 МБ из-за неоптимизированных фотографий. Порядок работ такой: сначала картинки и сервер, потом скрипты, потом уже тонкая настройка.
Как это сделать в WordPress без плагина
В WordPress все корректно подключённые скрипты проходят через wp_enqueue_script, а значит их HTML можно перехватить фильтром. Код кладётся в functions.php темы или в mu-plugin.
add_filter('script_loader_tag', function ($tag, $handle, $src) {
// Что откладываем через defer, порядок между ними сохранится
$defer = ['jquery-core', 'jquery-migrate', 'theme-main', 'slider', 'wp-embed'];
// Что грузим асинхронно, зависимостей нет ни в одну сторону
$async = ['yandex-metrika'];
if (in_array($handle, $async, true)) {
return str_replace(' src=', ' async src=', $tag);
}
if (in_array($handle, $defer, true) && strpos($tag, ' defer') === false) {
return str_replace(' src=', ' defer src=', $tag);
}
return $tag;
}, 10, 3);
Белый список вместо «отложить всё» это принципиальный момент. Плагины делают наоборот: откладывают всё, а вы потом добавляете исключения по мере обнаружения поломок. Обнаружение обычно происходит через жалобу клиента, что заявки не приходят.
Узнать реальные handle можно одной строкой в консоли WP-CLI или временным дампом:
wp eval 'global $wp_scripts; wp_head(); print_r(array_keys($wp_scripts->registered));' --url=https://example.ru
Ещё одна полезная мелочь: wp-embed.min.js подключается на каждой странице и нужен только для встраивания чужих постов WordPress. На 95 процентах сайтов его можно просто выключить через wp_deregister_script('wp-embed'). Минус один запрос и полтора килобайта, зато бесплатно и без риска.
В Битриксе логика похожая, но проще: BitrixMainPageAsset::getInstance()->addString() позволяет добавить тег с нужными атрибутами вручную, а в настройках главного модуля есть опция перемещения JS в конец страницы. У Tilda своих ручек мало, там отложить получится только чужие вставки в блоке T123, а собственный tilda-blocks-*.js трогать бессмысленно и рискованно.
Когда плагин всё-таки удобнее
Не буду делать вид, что руками всегда лучше. Плагин выигрывает в трёх ситуациях.
Первая: сайт на shared-хостинге с LiteSpeed, где уже стоит LiteSpeed Cache. У него есть готовые переключатели «Load JS Deferred» и «Delay JS» с полем исключений, и он бесплатный. Настроить его аккуратно быстрее, чем писать фильтры.
Вторая: у владельца нет разработчика на постоянной основе, а тема обновляется. Правки в functions.php при обновлении темы улетают. Мера защиты это дочерняя тема или mu-plugin, но если этого нет, плагин переживёт обновление.
Третья: нужен не просто defer, а полноценная отложенная загрузка всего JS до первого взаимодействия. Руками это писать долго, а WP Rocket (порядка 5 500 рублей в год за одну лицензию) или Flying Scripts (бесплатный) делают это из коробки.
Против плагина есть один сильный аргумент. Каждый такой плагин это ещё и своя логика кэширования, конфликты с другими плагинами и лишняя сущность в обновлениях. У меня в практике был случай, когда после автообновления плагина оптимизации на сайте отвалилась корзина: изменился алгоритм определения инлайновых зависимостей. Как вычислять такие вещи, я разбирал в материале про то, какой плагин тормозит сайт.
Компромисс, который я обычно предлагаю клиентам: базовый defer прописан в mu-plugin руками (это надёжно и не ломается), а Delay JS для сторонних виджетов делается плагином, потому что там логика сложнее и правится чаще.
Чек-лист перед выкаткой
Прогоняйте по нему каждый раз, он экономит вечера.
- Список всех скриптов страницы собран, у каждого понятно назначение и зависимости.
- Инлайновые блоки, зависящие от внешних файлов, найдены и обёрнуты в
DOMContentLoadedили перенесены. - Платёжка, коллтрекинг и A/B тесты в белом списке исключений.
- Метрика оставлена на
async, дальше не откладывается. - Проверены руками: главная, страница услуги, карточка товара, корзина, страница контактов, форма заявки на всех трёх.
- Консоль браузера чистая на каждой проверенной странице, включая мобильную ширину.
- Замер Lighthouse сделан до и после в одинаковых условиях, цифры записаны.
- Тестовая заявка реально дошла до почты и Telegram.
- Метрика зафиксировала визит и цель, проверено в отчёте по времени.
- Есть бэкап или git-коммит, к которому можно откатиться за минуту.
Последние два пункта пропускают чаще всего, и именно они спасают. Ускоренный сайт, который перестал принимать заявки, это минус, а не плюс.
Что делать дальше
Если у вас в PageSpeed висит красное «Устраните ресурсы, блокирующие отображение», начните с малого: соберите список блокирующих файлов, отложите через defer свой код темы и jQuery, замерьте. На типовом сайте малого бизнеса это полчаса работы и 15-25 баллов мобильного PageSpeed. Ленивые виджеты и счётчики это второй заход, там нужен разговор с владельцем про то, чем можно жертвовать в аналитике.
Если разбираться самому некогда, я делаю такую работу отдельной задачей: инвентаризация скриптов, белый список, правки в mu-plugin, ленивый чат и карта, замеры до и после в отчёте. На среднем сайте это 2-4 часа. У клиентов на абонентке (годовой тариф 35 000 рублей, помесячный от 15 000) такие правки входят в часы обслуживания, отдельно платить не нужно, условия есть в тарифах.
Пишите через форму на главной, если сайт тормозит на мобильных и нужно понять, что именно из этого лечится за один вечер, а что тянет за собой переделку темы. Остальные разборы по скорости лежат в рубрике про скорость.
Частые вопросы
Чем defer отличается от async
Оба не блокируют разбор страницы. Defer выполняет скрипты после разбора и в том порядке, в каком они стоят. Async выполняет сразу по загрузке, в произвольном порядке. Зависимый код с async ломается.
Что откладывать нельзя
Код, который влияет на первый экран: скрипт установки темы оформления, инлайновую логику, от которой зависит вёрстка. Отложите такое, и получите мигание страницы через 200 мс после отрисовки.
Как отложить тяжёлые виджеты
Грузить их по прокрутке или по клику. Чат, карта и видео весят сотни килобайт и нужны единицам посетителей, а замедляют они всех.
Съедает ли отложенная загрузка данные счётчиков
Может, если посетитель уходит раньше, чем счётчик загрузился. Для аналитики выбирают компромисс: не откладывать надолго, но и не грузить синхронно в шапке.