Техническое SEO
JavaScript SEO: как Google индексирует SPA и React-сайты

Двухволновое индексирование, ловушки CSR, разница SSR/SSG/ISR и практические способы убедиться, что JS-сайт правильно попадает в индекс.
Как Google рендерит JavaScript: принцип работы
Для обычной HTML-страницы Google делает один шаг: скачивает HTML и сразу видит контент. Для JavaScript-страницы шагов три: скачать HTML → загрузить JS-бандл → запустить его в headless Chromium, чтобы получить финальный DOM.
Google использует собственный браузерный движок на базе Chromium — актуальной версии, обновляемой регулярно. Поэтому современные JavaScript-фичи (ES2020+, dynamic import, WebAssembly) Googlebot понимает. Проблема не в поддержке синтаксиса, а в задержке рендеринга и ресурсных ограничениях обхода.
Рендеринг в цифрах
Таймаут рендеринга
Если JS не исполнился за ~3–4 секунды, Google индексирует частично отрендеренную страницу
Время на CSS
Ресурсы, загружаемые дольше 150 мс, могут блокировать рендеринг и ухудшать краулинг
Индексирование
Контент из JS попадает в индекс с задержкой относительно HTML-контента — иногда дни и недели
Evergreen Google
Googlebot обновляется до последней стабильной версии Chromium — JS-фичи поддерживаются полностью
Двухволновое индексирование: почему JS-контент попадает в индекс позже
Google разделяет обход на два этапа. Первая волна — краулинг: Googlebot скачивает HTML и все обнаруженные ссылки. Вторая волна — рендеринг: робот ставит страницы в очередь на рендеринг, запускает JS и индексирует финальный DOM.
Между этими волнами может пройти от нескольких часов до нескольких недель — в зависимости от авторитетности сайта, краулингового бюджета и размера очереди рендеринга Google. Если ваши ключевые тексты, заголовки H1, мета-теги или ссылки генерируются только через JS — они попадут в индекс с задержкой.
CSR — главная ловушка для SEO
Client-Side Rendering (CSR) — это когда сервер отдаёт пустой <div id="root"></div>, а весь HTML строится в браузере после загрузки JS-бандла. React, Vue и Angular в режиме SPA по умолчанию работают именно так.
С точки зрения SEO CSR — проблема по нескольким причинам: 1) рендеринг происходит только во второй волне; 2) ресурсы, заблокированные краулером (сторонние скрипты, CDN с robots.txt), не загружаются — страница рендерится неполностью; 3) JS-ошибка на любом этапе = пустая страница в индексе.
<!-- CSR: что видит Google в первой волне -->
<html>
<head>
<title></title> <!-- пусто -->
</head>
<body>
<div id="root"></div> <!-- контента нет -->
<script src="/bundle.js"></script>
</body>
</html><title>, <meta name="description"> и <h1> только через JS на клиенте. Google получит пустые или дефолтные значения при первичном краулинге и проиндексирует страницу без них.SSR, SSG и ISR: что выбрать для SEO
Решение проблем CSR — серверная генерация HTML. Существует три основных подхода, и каждый оптимален для своего сценария.
Сравнение подходов рендеринга
| Подход | HTML на сервере | SEO | Динамика |
|---|---|---|---|
| CSR | Нет | Плохо | Полная |
| SSR | Да (каждый запрос) | Отлично | Высокая |
| SSG | Да (при сборке) | Отлично | Нет (ребилд) |
| ISR | Да (по таймеру) | Отлично | Через ревалидацию |
| Подход | Подходит для |
|---|---|
| CSR | Авторизованные SPA, dashboards |
| SSR | Персонализированные страницы, каталоги |
| SSG | Блог, лендинги, документация |
| ISR | Каталоги с ценами, новости |
SSR (Server-Side Rendering) — HTML генерируется на сервере при каждом запросе. Максимальная актуальность данных, но нагрузка на сервер выше. Next.js: функция generateMetadata и async компоненты в App Router дают SSR по умолчанию.
SSG (Static Site Generation) — HTML генерируется один раз при сборке и отдаётся как статический файл. Самый быстрый TTFB, идеально для SEO. Минус: при изменении данных нужен ребилд.
ISR (Incremental Static Regeneration) — гибрид: страница строится статически, но по истечении времени ревалидации (revalidate: 3600) сервер перегенерирует её в фоне при следующем запросе. В Next.js это стандартный подход для большинства страниц.
// Next.js App Router — SSG + ISR
export const revalidate = 3600; // ревалидация каждый час
export async function generateMetadata(): Promise<Metadata> {
return {
title: 'Каталог товаров',
description: 'Весь ассортимент с актуальными ценами',
};
}
// Весь критичный контент рендерится на сервере:
export default async function CatalogPage() {
const products = await fetchProducts(); // серверный fetch
return (
<main>
<h1>Каталог товаров</h1>
{products.map(p => <ProductCard key={p.id} {...p} />)}
</main>
);
}Динамические ссылки и навигация
Googlebot понимает ссылки только в формате <a href="/url">. Ссылки, реализованные через onClick, button, data-href или window.location — для краулера невидимы. Это означает, что страницы, доступные только через такую навигацию, могут не попасть в индекс.
- Используйте
<a href>для всех навигационных ссылок — неbutton onClickсrouter.push() - Пагинация через URL-параметры (
?page=2) — Googlebot обходит такие URL, если они присутствуют в HTML - SPA-роутер: Next.js
<Link>и React Router<Link>генерируют корректные<a>теги на сервере - Хэш-навигация (
#anchor) не создаёт отдельных URL в индексе — не используйте для контента, который должен ранжироваться отдельно - Динамические меню: если пункты меню рендерятся через API после загрузки — Google может их не обнаружить, пока не отрендерит страницу
Lazy load и бесконечный скролл
Изображения с loading="lazy" Googlebot загружает — современный краулер поддерживает ленивую загрузку. Но есть условие: изображение должно быть в DOM с корректным атрибутом src при SSR. Если src подставляется JavaScript-ом уже в браузере — краулер видит пустой тег.
Бесконечный скролл — классическая проблема. Google может загружать первый «экран» контента, но последующие порции, подгружаемые через IntersectionObserver, не гарантированно индексируются. Решение: дублируйте контент через пагинацию с реальными URL (/catalog?page=2) или добавляйте ссылки на подгружаемые страницы в HTML.
| Паттерн | Googlebot видит? | Рекомендация |
|---|---|---|
| <img src="/img.webp" loading="lazy"> | Да | Использовать |
| <img data-src="/img.webp"> (JS подставляет src) | Нет | Избегать |
| Бесконечный скролл без URL | Только первый экран | Добавить пагинацию |
| Контент за кнопкой «Показать ещё» | Нет | Показывать часть в HTML |
| Tabbed content (скрытые вкладки) | Да, с задержкой | Дублировать в HTML |
Диагностика: как проверить рендеринг JavaScript
Ключевой вопрос диагностики: «Что видит Googlebot на моей странице?». Существует несколько инструментов для ответа на него.
Инструменты диагностики
- Google Search Console → Инспекция URL: показывает скриншот страницы после рендеринга Googlebot-ом, список ресурсов и обнаруженных ссылок. Самый прямой способ проверки.
- Просмотр «как Googlebot» в GSC: запускает реальный краулер и возвращает HTML после рендеринга — можно сравнить с исходным HTML.
curlилиwget: получить сырой HTML без JS. Если ключевые мета-теги и контент есть вcurl-выводе — они будут в первой волне индексирования.- Отключить JS в Chrome DevTools (F12 → Ctrl+Shift+P → «Disable JavaScript»): быстрый способ увидеть страницу глазами краулера без рендеринга.
- Fetch as Google в GSC: аналог предыдущего метода, но с реальным UA Googlebot-а и рендерингом в очереди Google.
- Screaming Frog с рендерингом: настройка «Spider > Configuration > Rendering > Googlebot» позволяет краулить сайт с рендерингом JS — видны ссылки, найденные после рендеринга.
# Проверить, что Google видит без рендеринга JS:
curl -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://example.com/page
# Найти все title / meta description / h1 в HTML-ответе:
curl -s https://example.com/page | grep -E '<title>|<meta name="description"|<h1'Чеклист JavaScript SEO
Основные проверки
- Title и meta description присутствуют в HTML-ответе сервера (SSR/SSG), не генерируются JS
- H1, H2 и основной текстовый контент доступны без JavaScript
- Ссылки навигации реализованы через
<a href>, а неonClick/button - Структурированные данные (JSON-LD) присутствуют в
<head>в HTML-ответе - Изображения имеют статичный
srcв HTML,loading="lazy"— только для не-LCP изображений - Robots.txt не блокирует JS/CSS файлы, необходимые для рендеринга
- Нет JS-ошибок, ломающих рендеринг (проверьте Chrome DevTools Console на странице)
- Core Web Vitals: LCP, INP, CLS соответствуют пороговым значениям Google
- Пагинация через URL-параметры, не только через JS-состояние
- Канонические URL (
<link rel="[canonical](/glossary/canonical)">) в HTML, не устанавливаются через JS