Откройте 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-видео это легко экономит мегабайт-другой трафика и пачку запросов до сторонних серверов.
Для скриптов, которые вообще не нужны прямо сейчас и могут подождать конца всех важных дел, есть более тонкий инструмент — 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 секунд браузер вызовет колбэк принудительно. Полезно для чатов, апселл-баннеров, всего, что приятно иметь, но без чего жить можно.
Откуда грузить: 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 генерируется новый, угадать его атакующему невозможно.
Что именно грузим: 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 — не каприз, а единственный рабочий вариант.
Где грузим: 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 зеленеет, юристы перестают звонить, а пользователи реже жалуются на «тормозит».
Комментарии (0)