Архітектура Single Page Applications (SPA) на базі React, Vue або Angular перенесла відповідальність за побудову Document Object Model (DOM) з веб-сервера на клієнтський браузер. Під час запиту сторінки сервер повертає мінімальний HTML-каркас:
<!DOCTYPE html>
<html lang="uk">
<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:
Різнорідність екосистеми краулерів:
/#/filter?category=shoes) замість реальних шляхів (/catalog/shoes) повністю приховує товари від краулера, оскільки WRS ігнорує URL-фрагменти за стандартом RFC 3986.Під час оцінки 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 та знімок екрана. Витягнутий текст і нові посилання відправляються в основний індекс.