Renderizado JavaScript en web scraping: cuándo lo necesitas y qué cuesta
Cómo saber si una página necesita un navegador real en vez de un fetch simple, por qué renderizar cuesta más, y los multiplicadores en créditos.

Un fetch HTTP simple te trae el HTML que envió el servidor. Cada vez más, esa no es la página que ve un visitante. La brecha entre las dos es el renderizado JavaScript, y es la razón número uno por la que un scraper que funciona en un sitio falla en silencio en el siguiente.
¿Cómo sabes si una página necesita renderizado?
Dos comprobaciones, ambas rápidas, antes de escribir cualquier código de scraping:
Compara el view-source con el DOM. En un navegador, el view-source (Ctrl+U o Cmd+Option+U) muestra exactamente lo que recibiría un fetch simple: el HTML en bruto del servidor, antes de que corra ningún script. Ahora abre las dev tools y mira el panel Elements, que muestra el DOM después de que se haya ejecutado JavaScript. Si el contenido que quieres, precios, listados, reseñas, está presente en Elements pero ausente o sustituido por un esqueleto de carga en el view-source, la página está construyendo ese contenido en el lado del cliente.
Tráela tú mismo sin navegador. La misma comprobación desde código, sin necesidad de dev tools:
import requests
resp = requests.get("https://example.com/products", timeout=30)
print(len(resp.text))
print("add-to-cart" in resp.text) # cámbialo por una cadena que sepas que está en el contenido real
Si una cadena marcadora que puedes ver en la página en vivo está ausente de resp.text, un fetch simple no te está trayendo el contenido, y necesitas algo que ejecute JavaScript antes de leer la página.
Una razón rápida de por qué esto pasa más que antes: los frameworks modernos (React, Vue, Svelte y compañía) envían un shell HTML casi vacío y construyen la página real en el navegador. Es una decisión de arquitectura deliberada para el sitio, no una medida anti-scraping, pero para ti produce el mismo resultado de todos modos.
¿Por qué cuesta más renderizar, en todas partes?
Un fetch simple es un socket, una solicitud, una respuesta. Renderizar significa levantar algo parecido a un navegador completo: cargar el DOM, ejecutar scripts, esperar a que se asienten las llamadas de red, a veces hacer scroll para disparar contenido de carga diferida. Eso es CPU y memoria reales por página, mantenidas abiertas durante todo el renderizado, no los microsegundos que tarda un fetch estático.
Esta estructura de costes no es específica de ningún proveedor. Todo proveedor que ofrece tanto un fetch estático como uno renderizado cobra más por el renderizado, porque es infraestructura genuinamente más cara de correr a escala: un pool de procesos de navegador real en vez de un pool de conexiones. Si un presupuesto para scraping renderizado sale al mismo precio que uno estático, mira de cerca qué significa “renderizado” en su documentación.
Lo que cuesta renderizar en FastAF
/scrape tiene precios en cuatro niveles, y el multiplicador sigue el coste de lo que realmente corre detrás de la solicitud:
| Modo | Parámetro | Créditos |
|---|---|---|
| Fetch estático | (por defecto) | 1 |
| Renderizado JS | dynamic=true |
5 |
| Proxies premium, sin renderizado | premium=true |
10 |
| Renderizado JS + proxies premium | dynamic=true&premium=true |
25 |
Estático a un salto de 5x para añadir renderizado, y un salto de 25x para añadir renderizado y enrutamiento por proxy premium a la vez. Ese es el coste real de la infraestructura extra, no un margen arbitrario, por lo que merece la pena comprobar si de verdad necesitas renderizado antes de usarlo por defecto.
La estrategia: prueba estático primero, escala solo si falla
La mayoría de los jobs de scraping mezclan páginas que necesitan renderizado con páginas que no, incluso dentro del mismo sitio. Pagar dynamic=true en cada solicitud cuando solo algunas páginas lo necesitan es la forma más común de gastar de más en este endpoint:
import requests
API_KEY = "faf_live_..." # tu clave de FastAF
def scrape(url, dynamic=False, premium=False, wait=None):
params = {"url": url}
if dynamic:
params["dynamic"] = "true"
if premium:
params["premium"] = "true"
if wait:
params["wait"] = wait
resp = requests.get(
"https://api.fastaf.co/v1/scrape",
params=params,
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=90,
)
resp.raise_for_status()
return resp.text
def scrape_smart(url, marker):
html = scrape(url)
if marker in html:
return html # con estático bastó, 1 crédito gastado
html = scrape(url, dynamic=True) # escalar, 5 créditos gastados
return html
marker es una cadena que sabes que aparece en el contenido real, un título de producto, un formato de precio, cualquier cosa que faltaría en una página shell. Si el fetch estático ya la tiene, te ahorraste 4 créditos en esa URL. Si no la tiene, escalas una vez, no por una corazonada.
Recurre a premium=true por separado: va de calidad de proxy y de esquivar bloqueos por IP, no de JavaScript. Un sitio puede necesitar uno, ambos o ninguno. wait (milisegundos) merece la pena fijarlo de forma explícita en solicitudes dynamic=true para páginas que cargan contenido después de que termine una llamada a la API, en vez de asumir que la espera por defecto basta.
La versión corta
Compara el view-source con el DOM renderizado, o trae la página tú mismo, antes de asumir que necesitas un navegador. Estático es 1 crédito, renderizar son 5, proxies premium son 10, ambos juntos son 25: los multiplicadores existen porque la infraestructura de navegador real cuesta genuinamente más de correr. Prueba estático primero y escala a dynamic=true solo en las URLs donde el contenido marcador realmente falta, y pagarás la prima de renderizado solo donde se la haya ganado.