Откройте DevTools на любом интернет-магазине, перейдите во вкладку Network, перезагрузите страницу и посмотрите на список запросов. С большой вероятностью половина из них — чужой JavaScript: Google Tag Manager, аналитика, чат-виджет на колёсиках, рекламные сети, пиксель соцсети, A/B-тестировщик, embed-плеер YouTube. Каждая такая строка — это код, который мы не писали, не контролируем и обновлять не можем, но который выполняется в нашем origin с теми же правами, что и наш собственный.

За это удобство мы платим тремя налогами. Первый — скорость: сторонний JS блокирует рендер, ест мобильный трафик, добивает Core Web Vitals. Второй — безопасность: компрометация чужого CDN превращается в backdoor на нашем сайте. Самый громкий пример — взлом British Airways в 2018 году: атакующие подменили один из подгружаемых скриптов и за пару недель собрали данные карт у 380 000 пользователей. Закончилось штрафом по GDPR на 20 миллионов фунтов. Третий налог — приватность: трекеры утаскивают данные посетителей раньше, чем те успели согласиться, и за это уже выписывают штрафы регуляторы.

Хорошая новость: у браузеров есть штатный набор инструментов, чтобы все три налога снизить почти до нуля. Разберём по очереди, не лезя в дебри.

С чего начать: аудит того, что уже стоит

Прежде чем что-то оптимизировать, надо понять, что вообще грузится. На реальном проекте обычно обнаруживается пара скриптов, которые ставили «на месяц для эксперимента» три года назад и забыли. Они до сих пор тянут килобайты и при этом не делают ничего.

Минимальный инструментарий, который есть прямо в браузере:

  • DevTools → Network → колонка Initiator. Показывает, какой скрипт инициировал каждый запрос. Удобно ловить цепочки «один тег подтянул второй, тот — третий».
  • DevTools → Coverage (Ctrl+Shift+P → «Show Coverage»). Покажет процент неиспользованного JS на каждом файле. Цифра в 80–90% на стороннем скрипте — повод задуматься, нужен ли он вообще.
  • Lighthouse → раздел Diagnostics → «Reduce the impact of third-party code». Лайтхаус сам соберёт сторонние домены и покажет, кто из них блокирует main-thread дольше остальных.
  • thirdpartyweb.today — публичный датасет от HTTP Archive с замерами производительности для 6000+ популярных сторонних скриптов. Полезно, чтобы заранее знать, во что обойдётся Hotjar, Intercom или очередной chat-виджет.

На каждый найденный скрипт честно задаём два вопроса: что он делает и что отвалится, если его убрать. Если на оба ответ «не уверен» — первый кандидат на вынос. Дальше уже работаем с теми, что остались.

Когда грузить: async, defer и ленивая инициализация

По умолчанию <script src="..."> ведёт себя максимально вежливо к разработчику и максимально вредно для пользователя: при парсинге HTML браузер останавливается, идёт качать скрипт, выполняет, и только потом продолжает строить страницу. На тяжёлом мобильном это — секунды белого экрана.

Два атрибута меняют поведение:

<!-- блокирует парсинг: плохо -->
<script src="https://cdn.example.com/widget.js"></script>

<!-- грузится параллельно, выполняется как только скачается -->
<script src="https://cdn.example.com/analytics.js" async></script>

<!-- грузится параллельно, выполняется после парсинга, порядок сохраняется -->
<script src="https://cdn.example.com/widget.js" defer></script>

Разница в одной фразе: async — «забрось и забудь, порядок не важен», defer — «запусти после того, как страница построится, и сохрани порядок».

Атрибут Когда грузится Когда выполняется Когда брать
(без атрибута) Сразу, блокируя парсинг Сразу после загрузки Почти никогда. Только если скрипт критически нужен до отрисовки.
async Параллельно с парсингом Сразу как скачался, прерывая парсинг Аналитика, пиксели, любые независимые трекеры. GA, Яндекс.Метрика, FB Pixel — учебный пример.
defer Параллельно с парсингом После полного парсинга, в порядке размещения в HTML Скрипты, которые трогают DOM или зависят друг от друга. Виджеты, инициализирующие интерфейс.

Типичный антипаттерн — навесить async на скрипт, у которого есть зависимость от другого скрипта, и потом удивляться, почему чат-виджет периодически «не подхватывается». У async порядок недетерминированный: что первое скачалось, то первое и стартанёт.

Для embed-ов, которые лежат в iframe (видео YouTube, твиты, карты), есть отдельная экономия — ленивая загрузка нативно, без библиотек:

<iframe
    src="https://www.youtube.com/embed/dQw4w9WgXcQ"
    loading="lazy"
    width="560" height="315"
    title="Демо"></iframe>

Браузер сам отложит запрос на сам iframe и весь его JS до того момента, пока пользователь не доскроллит. На странице с десятком embed-видео это легко экономит мегабайт-другой трафика и пачку запросов до сторонних серверов.

Поддержка браузерами
chrome
Chrome
77
firefox
Firefox
121
edge
Edge
79
safari
Safari
16.4
opera
Opera
64

Для скриптов, которые вообще не нужны прямо сейчас и могут подождать конца всех важных дел, есть более тонкий инструмент — requestIdleCallback. Он зовёт переданную функцию, когда у браузера действительно нет важных задач:

// Чат-виджет инициализируем, только когда странице нечего делать
const initChat = () => {
    const s = document.createElement('script');
    s.src = 'https://cdn.example.com/chat.js';
    s.async = true;
    document.body.appendChild(s);
};

if ('requestIdleCallback' in window) {
    requestIdleCallback(initChat, { timeout: 5000 });
} else {
    // Safari пока не умеет — ложимся на setTimeout
    setTimeout(initChat, 2000);
}

Опция timeout — страховка: даже если «спокойный момент» так и не наступил, через 5 секунд браузер вызовет колбэк принудительно. Полезно для чатов, апселл-баннеров, всего, что приятно иметь, но без чего жить можно.

Поддержка браузерами
chrome
Chrome
47
firefox
Firefox
55
edge
Edge
79
safari
Safari
 
opera
Opera
34

Откуда грузить: CSP как белый список доменов

Допустим, скрипты мы рассортировали. Дальше начинается часть про безопасность. Главный вопрос: что произойдёт, если злоумышленник внедрит в шаблон страницы лишний <script src="https://evil.example/x.js">? По умолчанию — ничего хорошего: браузер послушно загрузит и выполнит.

Content Security Policy — HTTP-заголовок, которым сайт говорит браузеру: «разрешаю грузить скрипты только отсюда и оттуда, на всё остальное закрывай глаза и пиши в консоль ошибку». Минимальный пример:

Content-Security-Policy: script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;

Разбираем по частям. script-src — директива именно про скрипты (есть отдельные style-src, img-src и компания). 'self' в кавычках — разрешить скрипты только с того же origin, что и сама страница. Дальше — явный список доверенных хостов. Браузер откажется выполнить любой <script>, чей источник не входит в этот список, и зафиксирует попытку в консоли.

Настройка обычно делается на уровне веб-сервера или CDN. Для nginx:

add_header Content-Security-Policy "script-src 'self' https://www.googletagmanager.com; object-src 'none';" always;

Для apache:

Header always set Content-Security-Policy "script-src 'self' https://www.googletagmanager.com; object-src 'none';"

Есть и второй способ — через <meta http-equiv="Content-Security-Policy"> прямо в HTML, и в учебниках его часто показывают первым. Но meta-тег умеет не все директивы (например, frame-ancestors и report-uri через meta не работают), плюс к моменту его парсинга часть страницы уже могла отрендериться. Серьёзная CSP всегда едет заголовком.

Современный подход для сложных сайтов — не перечислять хосты, а ставить каждому инлайн-скрипту одноразовый nonce-токен и разрешать только их:

Content-Security-Policy: script-src 'nonce-r4nd0m123' 'strict-dynamic';

<script nonce="r4nd0m123">
    // только этот блок выполнится
</script>

Директива 'strict-dynamic' расширяет доверие на скрипты, которые этот nonce-скрипт сам подгружает дальше — так не нужно вручную перечислять весь длинный хвост CDN-ов аналитики. На каждый запрос nonce генерируется новый, угадать его атакующему невозможно.

Поддержка браузерами
chrome
Chrome
25
firefox
Firefox
23
edge
Edge
14
safari
Safari
7
opera
Opera
15

Что именно грузим: SRI и хеш содержимого

CSP отвечает за вопрос «откуда», но не за вопрос «что именно». Доверенный CDN может быть скомпрометирован — и тогда сайт послушно подтянет вредоносную версию доверенной библиотеки. Та самая история с British Airways из вступления — ровно про этот сценарий.

Subresource Integrity (SRI) добавляет к тегу скрипта хеш ожидаемого содержимого. Если файл изменился хотя бы на байт — хеш не совпадёт, и браузер просто откажется его выполнить.

<script
    src="https://cdn.example.com/lib/utils-1.4.2.js"
    integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxC4c/8OxEX/XA5n/VvnMhvN++/L1uV"
    crossorigin="anonymous"></script>

Атрибут crossorigin="anonymous" обязателен — без него браузер не получит доступа к содержимому ответа для проверки хеша. Сам хеш считается через SHA-384 (можно SHA-256 и SHA-512, но 384 принято как баланс между длиной и стойкостью):

# из терминала
openssl dgst -sha384 -binary utils-1.4.2.js | openssl base64 -A

Или через веб-сервис srihash.org — вставить URL, получить готовый тег.

Есть один важный нюанс, про который часто забывают: SRI работает только с версионированными URL. Если поставить хеш на ссылку вида https://cdn.example.com/lib/latest.js, в момент очередного обновления библиотеки на CDN скрипт перестанет загружаться у всех пользователей — реальный хеш изменится, прописанный останется старым. Поэтому привычка сначала переключаться на конкретную версию вроде utils-1.4.2.js, и только потом вешать SRI — не каприз, а единственный рабочий вариант.

Поддержка браузерами
chrome
Chrome
45
firefox
Firefox
43
edge
Edge
17
safari
Safari
11.1
opera
Opera
32

Где грузим: sandbox для совсем недоверенного контента

Иногда сторонний код нельзя подвинуть ни на async, ни на CSP — например, готовый embed-блок из рекламной сети, который запихивает к себе свой DOM, свои стили и свои обработчики. Лучшее, что можно сделать в этом случае — не пускать его в основной document, а посадить в <iframe> с атрибутом sandbox:

<iframe
    src="https://ads.example.com/banner"
    sandbox="allow-scripts allow-popups"
    loading="lazy"
    width="300" height="250"></iframe>

Пустой sandbox="" — это максимальные ограничения: запрещены скрипты, формы, всплывающие окна, top-level навигация, доступ к localStorage и cookies родительского сайта. Каждое значение allow-* точечно ослабляет одно из ограничений. Полезно держать таблицу под рукой:

Значение Что разрешает
allow-scripts Выполнение JavaScript внутри iframe. Без него embed рекламы или плеера, скорее всего, не запустится.
allow-forms Отправка форм наружу.
allow-popups window.open и target="_blank". Нужно для рекламы, которая открывает рекламодателя.
allow-same-origin Считать содержимое iframe как тот же origin. Серьёзная уступка: открывает доступ к cookies и localStorage родителя.
allow-top-navigation Iframe может менять location родительского окна. Антипаттерн в большинстве случаев.
allow-modals Разрешает alert(), confirm(), prompt().
allow-presentation Доступ к Presentation API (трансляция на внешние экраны).

Принцип простой: начинаем с пустого sandbox="" и добавляем allow-* по одному только тогда, когда что-то конкретное реально перестало работать.

Особенно осторожно с комбинацией allow-scripts allow-same-origin — вместе они означают, что код внутри iframe и работает, и видит ваши cookies. Это уровень обычного <script> без sandbox, только в красивой обёртке. Если разрешаете оба — убедитесь, что доверяете источнику примерно как самому себе.

Сбор данных: согласие и приватность

Технический пайплайн на этом закончен, но есть ещё юридический. GDPR в Европе и CCPA в Калифорнии (а в большинстве других регионов — схожие законы) требуют, чтобы сайт не выстреливал трекеры и cookies, идентифицирующие пользователя, до того как тот явно согласился. Штрафы за нарушение измеряются процентами от годового оборота — не та категория, где удобно полагаться на «никто не заметит».

В коде это выглядит так: при первом визите страница рисует cookie-баннер, а реальный <script src="ga.js"> вставляется в DOM только после клика по «Принять»:

function loadAnalytics() {
    if (localStorage.getItem('consent') !== 'granted') return;
    const s = document.createElement('script');
    s.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXX';
    s.async = true;
    document.head.appendChild(s);
}

document.querySelector('#accept-cookies').addEventListener('click', () => {
    localStorage.setItem('consent', 'granted');
    loadAnalytics();
});

// если согласие уже давалось — не дёргаем баннер заново
if (localStorage.getItem('consent') === 'granted') {
    loadAnalytics();
}

Для GA и Метрики есть и более деликатный режим: те же скрипты можно загрузить сразу, но включить опцию анонимизации IP (anonymize_ip: true в GA, ut-параметр в Метрике) — так данные не привязываются к личности пользователя, и формально такой сбор под GDPR попадает в более мягкую категорию.

Радикальный путь — cookieless-аналитика: Plausible, Umami, Cloudflare Web Analytics. Они вообще не ставят cookies и не сохраняют идентификаторы посетителей, собирая только агрегированные метрики (страница, реферер, страна, устройство). Cookie-баннер при этом не нужен, что и для пользователей приятнее, и юристам спать спокойнее. Главный минус — уровень аналитики проще, чем у GA: воронок и attribution-моделей такого детального уровня там не будет.

Чеклист на проде

Короткий список того, что имеет смысл проверить на боевом сайте прямо сейчас:

  • Открыть DevTools → Network на главной и пройтись по сторонним скриптам — каждый ли нужен в 2026 году.
  • На скриптах без атрибутов проверить, нет ли возможности добавить async или defer без поломок.
  • На embed-iframe-ах с видео и постами соцсетей — повесить loading="lazy".
  • В заголовках ответа сервера — есть ли Content-Security-Policy хотя бы с script-src.
  • На скриптах с внешних CDN — есть ли integrity, и ведут ли URL на конкретную версию (а не на «latest»).
  • Для рекламы и недоверенных embed-блоков — iframe с минимально достаточным sandbox.
  • Аналитика и пиксели — стартуют до согласия пользователя или после.

Из этих семи пунктов первые два — пять минут работы, остальные — от часа до пары дней в зависимости от размера проекта. Окупается тем, что Lighthouse зеленеет, юристы перестают звонить, а пользователи реже жалуются на «тормозит».