Архитектура Single Page Applications (SPA) на базе React, Vue или Angular перенесла ответственность за сборку Document Object Model (DOM) с веб-сервера на клиентский браузер. При запросе страницы сервер отдает минимальный HTML-каркас:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/main.js"></script>
</body>
</html>Для классических поисковых роботов и социальных сетей такой ответ представляет собой информационный вакуум. Несмотря на то, что Googlebot с 2019 года использует «вечнозеленый» headless Chromium (Evergreen Chromium) для исполнения клиентского JavaScript, предположение о том, что «Google сам всё отрендерит и проиндексирует точно так же, как обычный браузер», является опасным инженерным заблуждением.
Главная проблема кроется в разделении процесса краулинга на две асинхронные фазы посредством сервиса Web Rendering Service (WRS). Рендеринг JavaScript требует колоссальных вычислительных мощностей (CPU, GPU, RAM), в тысячи раз превышающих парсинг сырого HTML. В результате поисковик экономит ресурсы инфраструктуры: страницы SPA встают в буфер отложенного рендеринга (Render Queue), из-за чего индексация динамического контента задерживается на дни или недели, а краулинговый бюджет сайтов быстро истощается.
+-------------------------------------------------------------------------+
| АРХИТЕКТУРА ИНДЕКСАЦИИ GOOGLEBOT WRS |
+-------------------------------------------------------------------------+
[ HTTP GET Запрос ]
|
v
+--------------+ Первичный HTML
| Краулер | -------------------------------------> [ Фаза 1: Индексация HTML ]
| (Googlebot) | (Статические теги, links, (Сырой DOM, <div id="root">,
+--------------+ метаданные сервера) зачастую без контента)
| |
| Сохранение HTML в базу |
v v
+--------------+ [ Индекс поисковика ]
| Render Queue | <--- Буфер ожидания CPU ресурсов ^
+--------------+ (Задержка: от часов до 3-4 недель) |
| |
v Выделение слота headless-браузера |
+--------------+ |
| WRS | ---> [ Выполнение JS / API fetch ] |
| (Chromium) | ---> [ Построение Rendered DOM ] -----------------+
+--------------+ Фаза 2: Вторичная индексация
(Обновление сниппета, ссылок)Чтобы принимать взвешенные архитектурные решения при проектировании клиентских приложений, необходимо понимать три фундаментальных ограничения:
Двухфазная индексация (Two-Wave Indexing):
<a href="...">. Если SPA не использует Server-Side Rendering (SSR) или Static Site Generation (SSG), в индекс попадает пустая страница либо базовый fallback загрузчика (spinner).Render Queue. Когда освобождаются вычислительные кластеры WRS, headless Chromium загружает JavaScript-бандлы, исполняет их, выполняет сетевые запросы fetch(), формирует Rendered DOM и передает его в индекс. Разрыв во времени между первой и второй фазами называется Rendering Delay (задержка рендеринга).Краулинговый бюджет (Crawl Budget) и ограничения CPU:
Разнородность экосистемы краулеров:
Почему поисковый робот Googlebot не индексирует динамический контент React SPA синхронно в момент первого запроса страницы?
/#/filter?category=shoes) вместо реальных путей (/catalog/shoes) полностью скрывает товары от краулера, так как WRS игнорирует URL-фрагменты.При оценке SPA инженером по веб-производительности ключевой метрикой является прозрачность приложения для краулера. Рассмотрим типовые сетевые и архитектурные барьеры WRS.
Headless Chromium при исполнении React-бандла запрашивает все вспомогательные ассеты: .js, .css, а также эндпоинты внутренних API (/api/v1/products/...). Если файл robots.txt запрещает скачивание API или статики, WRS получает ошибку сетевого запроса, страница не может выполнить гидратацию, и поисковик индексирует пустой экран.
# НЕПРАВИЛЬНО: Блокировка API ломает клиентский рендеринг в WRS
User-agent: *
Disallow: /api/
Disallow: /assets/
Disallow: /static/
# ПРАВИЛЬНО: Разрешение краулеру загружать ассеты и публичные данные
User-agent: *
Allow: /assets/
Allow: /static/
Allow: /api/v1/public/
Disallow: /api/v1/private/
Disallow: /admin/
Sitemap: https://agenticlab.dev/sitemap.xmlДля верификации того, что видит WRS до и после выполнения скриптов, используется Playwright или Puppeteer с отключенным кэшем и строгим таймаутом выполнения скриптов:
import { chromium, type Browser, type Page } from 'playwright';
interface AuditResult {
url: string;
hasRootContentBeforeJs: boolean;
hasContentAfterJs: boolean;
jsExecutionTimeMs: number;
blockedRequests: string[];
pageTitle: string;
h1Count: number;
}
export async function auditWrsRendering(targetUrl: string): Promise<AuditResult> {
const browser: Browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
});
const page: Page = await context.newPage();
const blockedRequests: string[] = [];
// Фиксируем сетевые сбои критических API
page.on('requestfailed', (req) => {
blockedRequests.push(`${req.method()} ${req.url()} - ${req.failure()?.errorText || 'Unknown'}`);
});
// 1. Проверяем сырой HTML (Фаза 1 WRS)
const rawResponse = await fetch(targetUrl, {
headers: { 'User-Agent': 'Googlebot/2.1' },
});
const rawHtml = await rawResponse.text();
const hasRootContentBeforeJs = !rawHtml.includes('<div id="root"></div>') && rawHtml.includes('<h1');
// 2. Проверяем Rendered DOM (Фаза 2 WRS)
const startTime = performance.now();
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 10000 });
const jsExecutionTimeMs = Math.round(performance.now() - startTime);
const pageTitle = await page.title();
const h1Count = await page.locator('h1').count();
const hasContentAfterJs = h1Count > 0;
await browser.close();
return {
url: targetUrl,
hasRootContentBeforeJs,
hasContentAfterJs,
jsExecutionTimeMs,
blockedRequests,
pageTitle,
h1Count,
};
}Google Search Console предоставляет инструмент URL Inspection (Проверка URL):
<div id="root"></div>.Other error или Blocked by robots.txt указывает на узкие места, ломающие выполнение клиентского кода.Если React-компонент на первом рендере проверяет localStorage.getItem('token') или сессию в IndexedDB и блокирует отображение основного контента до получения данных, WRS откроет страницу в чистом профиле без сохраненных данных хранилищ и проиндексирует пустой экран или форму авторизации.
Решение: Весь публичный контент страницы должен строиться исключительно на базе URL и серверных данных без предположений о состоянии клиентских хранилищ.
WRS выделяет на выполнение JS страницы жесткий лимит времени (как правило, до 5–10 секунд ответа сети и CPU). Если ваш React-компонент делает каскад вызовов: fetchUser() -> fetchPermissions() -> fetchCatalog() -> fetchItems(), суммарное время ожидания превысит квоту WRS, скрипт будет принудительно прерван, а страница останется недорендеренной.
Решение: Объединяйте запросы первого экрана в один агрегирующий API-эндпоинт (BFF / GraphQL) или используйте предварительный рендеринг (Static Snapshots / SSR).
Использование createHashRouter или ссылок вида example.com/#/products/42 в публичной части приложения. Поисковые краулеры по спецификации RFC 3986 отсекают фрагмент (#...) перед отправкой HTTP-запроса. Для поисковика все адреса с хэшем склеиваются в единый URL example.com/.
Решение: Всегда использовать HTML5 History API (BrowserRouter, pushState, replaceState) с чистыми путями и корректной маршрутизацией на веб-сервере.
Хотя Googlebot использует Evergreen Chromium, WRS запускается с жесткими флагами песочницы: отсутствуют WebGL, WebRTC, датчики движения, доступ к камере/микрофону, а также контексты Service Worker в ряде сценариев. Необработанное исключение (Uncaught Exception) на верхнем уровне React-дерева приводит к падению всего приложения (White Screen of Death).
Решение: Оборачивать компоненты первого экрана в строгие React Error Boundaries и экранировать вызовы нестандартных Web API проверками typeof window !== 'undefined' && 'serviceWorker' in navigator.
Googlebot запрашивает URL. Сервер отдает 200 OK и базовый HTML shell. Краулер немедленно парсит статические метаданные и извлекает ссылки <a href>.
Поскольку HTML не содержит достаточного контента, URL направляется в очередь WRS. Задержка может составлять от нескольких часов до 3-4 недель в зависимости от авторитетности сайта.
WRS выделяет виртуальную среду Chromium. Загружаются JS-бандлы, запускается React-движок, монтируются компоненты, выполняются вызовы fetch() к публичным API.
По истечении networkidle (или таймаута 5-10 секунд) WRS формирует сериализованный HTML и снимок экрана. Извлеченный текст и новые ссылки отправляются в основной индекс.
Какая критическая конфигурация в файле robots.txt гарантированно приведет к тому, что Googlebot проиндексирует пустой экран вместо React SPA?