AdSense убивает ваш Core Web Vitals? Как мы ускорили загрузку рекламы в 3 раза, перенеся SSR на Google Cloud Run
Больше баннеров богу баннеров, или Как мы уронили Lighthouse
Каждый медийный проект или контентный сайт рано или поздно сталкивается с одной и той же дилеммой. С одной стороны, вам нужна монетизация. Вы вешаете блоки Google AdSense, радуетесь первым центам, а потом бизнес говорит: «А давайте добавим еще вот сюда sticky-баннер, и вот сюда межстраничник». С другой стороны, есть SEO-шники, которые с пеной у рта кричат про Core Web Vitals (CWV) — метрики качества Google, от которых зависит позиция в поиске.
В чем проблема? Скрипты AdSense (да и любой другой программатик-рекламы) — это тяжеленные куски чужого JavaScript, которые загружаются асинхронно, плодят iframe-ы, блокируют главный поток браузера (Main Thread) и безбожно двигают контент (привет, Cumulative Layout Shift — CLS).
Мы разрабатываем крупный новостной портал на Next.js. Изначально мы рендерили все страницы на хилом сервере через классический SSR (Server-Side Rendering). Пока сайт был без рекламы, наш PageSpeed выдавал зеленые 95 баллов. Но как только мы включили AdSense и обвешались метриками, Lighthouse показал нам красные 35 баллов для мобилок. LCP (Largest Contentful Paint) улетел за 5 секунд, а TBT (Total Blocking Time) пробил потолок. Сайт начал тормозить, скролл лагал, а пользователи уходили, не дождавшись, пока загрузится статья.
Надо было срочно что-то делать. Отказаться от рекламы мы не могли (бизнес хочет кушать). Оптимизировать чужой скрипт adsbygoogle.js мы тоже не можем — это блэкбокс. Единственный выход — радикально изменить архитектуру отдачи контента.
Анатомия боли: Почему реклама ломает SSR
Давайте разберем, что происходит при загрузке страницы со стандартным SSR и рекламой.
- Пользователь делает запрос на сервер.
- Сервер (Node.js) собирает HTML (фетчит данные из базы, рендерит React-компоненты). На слабом сервере при высокой нагрузке это может занимать 300-500 мс.
- Браузер получает HTML и начинает его парсить.
- Браузер натыкается на теги
<script src="...adsbygoogle.js">и начинает качать рекламу. - Параллельно качается наш основной JS-бандл (React/Next.js) для гидратации.
- И тут начинается битва за Main Thread. Тяжелые скрипты рекламы начинают выполняться, блокируя процесс гидратации. Кнопки на сайте не нажимаются, пока реклама не отрендерится.
Проблема усугубляется тем, что наш старый сервер часто уходил в своп от наплыва трафика (новости — штука непредсказуемая, кто-то запостил ссылку в Telegram-канал, и к к нам прилетело 10 000 человек за минуту). Из-за долгого ответа сервера (TTFB — Time to First Byte), браузер позже начинал качать рекламу, а значит, и деньги мы теряли, потому что юзер мог уйти до показа баннера.
План спасения: Миграция в облака и ленивая загрузка
Мы поняли, что нам нужны две вещи:
- Мощный и авто-масштабируемый SSR. Нам нужен был TTFB (время до первого байта) в районе 50-100 мс даже при пиковых нагрузках, чтобы браузер как можно быстрее получал HTML и начинал скачивать рекламные скрипты.
- Хитрая загрузка рекламы. Мы не можем позволить рекламе блокировать гидратацию и LCP.
В качестве новой инфраструктуры мы выбрали Google Cloud Run. Это serverless-платформа для контейнеров. Вы просто даете ей Docker-образ вашего Next.js/Node.js приложения, и она сама скейлит его от 0 до 1000 экземпляров за секунды в зависимости от трафика. Платите только за время обработки запроса. Идеально для новостных сайтов с «рваным» трафиком.
Этап 1: Перенос Next.js на Google Cloud Run
Миграция заняла минимум времени. Cloud Run нативно поддерживает Node.js-приложения. Мы написали простейший Dockerfile:
# Dockerfile для Next.js на Cloud Run
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Важный нюанс: мы использовали фичу output: 'standalone' в next.config.js, чтобы собрать минимальный образ без лишних node_modules. Это критично для быстрого холодного старта в Cloud Run.
Задеплоили это добро в Cloud Run, повесили сверху Google Cloud CDN для кэширования статики и тех страниц, которые редко меняются (ISR - Incremental Static Regeneration). Результат? TTFB упал с 400 мс до 60 мс. Сервер стал отвечать мгновенно. Мы готовы к любому DDoS-у или вирусному трафику.
DDoS по-казахски и накрутка конкурентов: Как отбить атаку ботов через Google Cloud Armor и вычистить мусорный трафик из GA4
Этап 2: Укрощение строптивой рекламы (Lazy Loading AdSense)
Быстрый сервер — это половина дела. Если мы сейчас просто вставим скрипты AdSense в <head>, мы всё равно убьем TBT. Поэтому мы применили тактику отложенной инициализации.
Мы не загружаем adsbygoogle.js до тех пор, пока пользователь не начнет скроллить страницу или не совершит любое другое взаимодействие (клик, движение мышью). Для мобильных устройств, где важен каждый килобайт при загрузке первого экрана, это критически важно.
Вот как мы реализовали ленивую загрузку на React/Next.js:
// Хук для ленивой загрузки скрипта AdSense
import { useEffect, useState } from 'react';
const useLazyAdSense = () => {
const [adLoaded, setAdLoaded] = useState(false);
useEffect(() => {
const loadAds = () => {
if (adLoaded) return;
const script = document.createElement('script');
script.src = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
script.async = true;
script.crossOrigin = 'anonymous';
script.dataset.adClient = 'ca-pub-XXXXXXXXXXXXXXX'; // Ваш ID
document.head.appendChild(script);
setAdLoaded(true);
// Отписываемся от событий, чтобы не грузить скрипт дважды
window.removeEventListener('scroll', loadAds);
window.removeEventListener('mousemove', loadAds);
window.removeEventListener('touchstart', loadAds);
};
// Слушаем первые взаимодействия пользователя
window.addEventListener('scroll', loadAds, { once: true, passive: true });
window.addEventListener('mousemove', loadAds, { once: true, passive: true });
window.addEventListener('touchstart', loadAds, { once: true, passive: true });
return () => {
window.removeEventListener('scroll', loadAds);
window.removeEventListener('mousemove', loadAds);
window.removeEventListener('touchstart', loadAds);
};
}, [adLoaded]);
return adLoaded;
};Инженерный лайфхак: используйте { passive: true } для event listeners, чтобы не блокировать скролл браузера. Это еще один плюсик в карму Core Web Vitals.
Этап 3: Победа над Cumulative Layout Shift (CLS)
Реклама загружается лениво — круто. Но когда она загрузится, она резко распирает блок <ins>, сдвигая контент вниз. Юзер читает текст, бац — всё уехало. Это убивает метрику CLS. Google за такое жестоко наказывает в поиске.
Решение простое, но почему-то многие о нем забывают. Нужно резервировать место под баннеры с помощью CSS. Мы заранее знаем размеры блоков AdSense (например, 300x250 для мобилок или адаптивные). Если блок адаптивный, мы задаем ему минимальную высоту, чтобы предотвратить прыжки.
/* CSS для предотвращения CLS */
.ad-container {
min-height: 250px; /* Резервируем место */
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f8f9fa; /* Плейсхолдер, пока баннер грузится */
}В React-компоненте это выглядит так:
const AdUnit = ({ slotId }) => {
useEffect(() => {
// Инициализация конкретного блока после загрузки скрипта
if (window.adsbygoogle && window.adsbygoogle.loaded) {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error("AdSense Error:", e);
}
}
}, []);
return (
<div className="ad-container my-8">
<ins
className="adsbygoogle"
style={{ display: 'block', minHeight: '250px' }}
data-ad-client="ca-pub-XXXXXXXXXXXXXXX"
data-ad-slot={slotId}
data-ad-format="auto"
data-full-width-responsive="true"
/>
</div>
);
};LCP (Largest Contentful Paint) до и после миграции
Сравнение времени отрисовки самого крупного контента в секундах.
Результаты: Зеленый Lighthouse и рост доходов
Что мы получили в итоге, объединив мощь Google Cloud Run и ленивую загрузку компонентов?
- Core Web Vitals в зеленой зоне. LCP упал с 5.2 сек до 1.8 сек. CLS стал равен нулю. TBT снизился в 4 раза, потому что главный поток свободен при первой загрузке.
- Сайт стал летать. Пользователи моментально получают контент, могут сразу кликать по ссылкам, открывать меню, не дожидаясь, пока рекламные скрипты решат, какой баннер им показать.
- Google счастлив, SEO растет. Позиции в выдаче (SERP) улучшились, так как Google теперь считает наш сайт быстрым и удобным.
- Доходы от AdSense не упали, а выросли! Вы спросите: «Но ведь реклама грузится позже, мы теряем показы?». Нет! Благодаря тому, что сервер отвечает за 60 мс (Cloud Run рулит), а юзер не ждет белого экрана, он быстрее начинает скроллить и взаимодействовать с сайтом, триггеря показ рекламы. Плюс, у нас улучшилась метрика Active View (процент баннеров, которые реально были в зоне видимости), за которую рекламодатели платят больше.
Core Web Vitals (TBT и CLS)
Влияние ленивой загрузки и CSS-резервирования на блокировку потока и сдвиги.
Резюме для суровых инженеров
Не пытайтесь оптимизировать неоптимизируемое (чужой JS). Измените правила игры. Перенесите тяжелую работу по рендерингу в облако с автоскейлингом (Cloud Run + CDN), чтобы отдавать HTML мгновенно. Заморозьте все некритичные скрипты до первого взаимодействия пользователя. Зарезервируйте пиксели через CSS, чтобы контент не прыгал.
В мире, где контент правит бал, а реклама за него платит, баланс между UX и монетизацией достигается только жесткой архитектурной дисциплиной. И да, Cloud Run — это пушка. Мы больше не просыпаемся по ночам от алертов о том, что у нас упал сервер от вирусного трафика. Он просто скейлится, пережевывает запросы, отдает HTML и сворачивается обратно. Красота!

Рустам Шарафутдинов
Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).