La arquitectura de las Single Page Applications (SPA) basadas en React, Vue o Angular trasladó la responsabilidad de construir el Document Object Model (DOM) desde el servidor web hacia el navegador del cliente. Al solicitar una página, el servidor entrega un documento HTML esquelético mínimo:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/main.js"></script>
</body>
</html>Para los motores de búsqueda tradicionales y los scrapers de redes sociales, esta respuesta inicial constituye un vacío informativo absoluto. Aunque Googlebot emplea desde 2019 un motor headless Chromium perenne (Evergreen Chromium) para ejecutar JavaScript en el cliente, asumir que «Google renderizará e indexará todo exactamente igual que un navegador convencional» representa una peligrosa ilusión de ingeniería.
El cuello de botella crítico reside en la división del rastreo en dos fases asíncronas orquestadas por el Web Rendering Service (WRS). La ejecución de JavaScript demanda ingentes cantidades de recursos computacionales (CPU, GPU y memoria RAM), órdenes de magnitud superiores al simple análisis de HTML estático. En consecuencia, el buscador ahorra recursos relegando las URLs de SPAs a una cola de renderizado diferido (Render Queue), lo que retrasa la indexación del contenido dinámico durante días o semanas y agota aceleradamente el presupuesto de rastreo.
+-------------------------------------------------------------------------+
| ARQUITECTURA DE INDEXACIÓN DE GOOGLEBOT WRS |
+-------------------------------------------------------------------------+
[ Petición HTTP GET ]
|
v
+--------------+ HTML Crudo Inicial
| Rastreador | -------------------------------------> [ Fase 1: Indexación HTML ]
| (Googlebot) | (Cabeceras estáticas, enlaces, (DOM esquelético, <div id="root">,
+--------------+ metadatos del servidor) habitualmente carente de texto)
| |
| Almacenamiento de HTML en base de datos |
v v
+--------------+ [ Índice del Buscador ]
| Render Queue | <--- En espera de capacidad de cómputo ^
+--------------+ (Retardo: horas a 3-4 semanas) |
| |
v Asignación de instancia headless Chromium |
+--------------+ |
| WRS | ---> [ Ejecución de JS / Peticiones fetch API ] |
| (Chromium) | ---> [ Construcción del Rendered DOM ] -----------+
+--------------+ Fase 2: Indexación Secundaria
(Actualización de snippets y enlaces)Para tomar decisiones arquitectónicas fundadas al diseñar aplicaciones renderizadas en el cliente, los ingenieros deben dominar tres principios operacionales:
Indexación en Dos Fases (Two-Wave Indexing):
<a href="...">. Si la SPA no utiliza Server-Side Rendering (SSR) o Static Site Generation (SSG), el índice almacena una página en blanco o un spinner de carga.Render Queue. Cuando los clústeres de WRS disponen de cómputo libre, headless Chromium descarga los bundles de JavaScript, ejecuta el código de React, resuelve las llamadas fetch(), genera el Rendered DOM y lo incorpora al índice. La brecha temporal entre ambas fases se denomina Rendering Delay (retraso de renderizado).Presupuesto de Rastreo (Crawl Budget) y Restricciones de CPU:
Heterogeneidad en el Ecosistema de Rastreadores:
/#/filter?category=shoes) en lugar de rutas canónicas (/catalog/shoes) oculta por completo los productos a los rastreadores, ya que el WRS descarta los fragmentos conforme a la RFC 3986.Al evaluar la visibilidad de una SPA, la métrica clave es la transparencia de la aplicación para los rastreadores. A continuación se examinan los patrones de fallo arquitectónicos y de red más comunes.
robots.txt#Headless Chromium requiere descargar todos los recursos asociados al bundle de React: archivos .js, hojas de estilo .css y llamadas a endpoints de APIs internas (/api/v1/products/...). Si el archivo robots.txt bloquea estas rutas, WRS registra fallos de red durante la hidratación e indexa una pantalla vacía.
# ❌ INCORRECTO: Bloquear APIs destruye la ejecución en WRS
User-agent: *
Disallow: /api/
Disallow: /assets/
Disallow: /static/
# ✅ CORRECTO: Permitir a los bots el acceso a recursos estáticos y endpoints públicos
User-agent: *
Allow: /assets/
Allow: /static/
Allow: /api/v1/public/
Disallow: /api/v1/private/
Disallow: /admin/
Sitemap: https://agenticlab.dev/sitemap.xmlPara contrastar la visión que obtiene WRS antes y después de la ejecución de scripts, se puede utilizar Playwright configurado con la identidad de Googlebot y límites estrictos de tiempo:
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[] = [];
// Registrar peticiones fallidas a APIs críticas
page.on('requestfailed', (req) => {
blockedRequests.push(`${req.method()} ${req.url()} - ${req.failure()?.errorText || 'Unknown'}`);
});
// 1. Inspeccionar HTML estático inicial (Fase 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. Inspeccionar Rendered DOM final (Fase 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 ofrece la herramienta Inspección de URLs para verificar el comportamiento de WRS:
<div id="root"></div>.Bloqueado por robots.txt o Error de script identifican anomalías que abortan la hidratación del cliente.Si un componente React comprueba localStorage.getItem('token') o la sesión en IndexedDB durante el primer renderizado e interrumpe la visualización hasta consultar el almacenamiento, WRS se ejecutará en un perfil limpio sin datos previos e indexará una pantalla vacía o un formulario de login.
Solución: Todo el contenido público inicial debe generarse a partir de la URL y datos provistos por el servidor, sin depender de almacenamientos locales.
WRS asigna una cuota estricta de tiempo de ejecución por página (habitualmente entre 5 y 10 segundos para red y CPU). Si los componentes encadenan peticiones (fetchUser() -> fetchPermissions() -> fetchCatalog() -> fetchItems()), el tiempo total sobrepasará el límite, WRS abortará la ejecución e indexará una vista incompleta.
Solución: Consolidar los datos requeridos en el primer render en un único endpoint agregador (BFF / GraphQL) o aplicar pre-renderizado.
Utilizar createHashRouter o enlaces del tipo example.com/#/products/42 en páginas indexables arruina el SEO. Bajo la norma RFC 3986, el fragmento (#...) se gestiona exclusivamente en el cliente y nunca se envía al servidor web. Los motores de búsqueda truncan los fragmentos, unificando todas las URLs bajo la raíz example.com/.
Solución: Usar siempre la API de History de HTML5 (BrowserRouter, pushState, replaceState) con rutas amigables y reescritura en el servidor web.
Aunque Googlebot integra Evergreen Chromium, WRS opera bajo restricciones severas: no dispone de WebGL, WebRTC, sensores de movimiento, acceso a micrófono/cámara ni de ciertos contextos de Service Worker. Cualquier excepción no controlada en la raíz de React provocará el colapso de toda la aplicación (pantalla blanca).
Solución: Envolver las rutas principales en React Error Boundaries y proteger el acceso a APIs de navegador con comprobaciones defensivas (typeof window !== 'undefined' && 'serviceWorker' in navigator).
Googlebot solicita la URL. El servidor responde con 200 OK y el cascarón HTML. El bot indexa de inmediato los metadatos estáticos y extrae los enlaces <a href>.
Al carecer de contenido textual en el HTML plano, la URL entra en la cola de WRS. El retraso oscila entre horas y varias semanas según la autoridad del sitio.
WRS asigna una instancia virtual de Chromium. Se descargan los bundles, se ejecuta React, se montan componentes y se resuelven llamadas fetch() a APIs públicas.
Tras alcanzar networkidle o cumplir el timeout de 5-10s, WRS captura el DOM serializado y el pantallazo. El texto y los nuevos enlaces actualizan el índice.