El reto del contenido remitido por JavaScript
Los sitios web modernos dependen cada vez más de JavaScript para renderizar contenido. Aplicaciones de una sola página (SPAs) construidas con React, Vue o Angular cargan un mínimo de shell HTML, luego capturar y renderizar datos lado cliente. Cuando usted hace una simple solicitud HTTP a estos sitios, usted obtiene una página vacía o incompleta porque el contenido sólo existe después de la ejecución de JavaScript.
Raspados sitios web de JavaScript-heavy requiere navegadores sin cabeza — motores de navegador reales corriendo sin una ventana visible que puede ejecutar JavaScript, renderizar DOM, e interactuar con elementos de página. Combinado con proxies, los navegadores sin cabeza desbloquean datos de incluso los sitios web más dinámicos.
Esta guía es parte de nuestra Guía completa de Proxies de Rastreo Web. Para evitar la detección mientras utiliza navegadores sin cabeza, vea Cómo los sistemas anticuerpos detectan proxies.
¿Cuándo necesitas un navegador sin cabeza?
| Escenario | HTTP simple | Navegador sin cabeza |
|---|---|---|
| Páginas HTML estaticas | Funciona perfectamente. | Overkill |
| Páginas remitidas por servidor con API | Works (hit the API directly) | No se necesita |
| SPA (React, Vue, Angular) | Consigue concha vacía | Necesidad |
| Pergamino infinito / carga perezosa | No se puede desencadenar | Necesidad |
| Contenido tras login + JS | Dificultad | Recomendado |
| Pages with anti-bot JS checks | Detección de fallas | Necesidad |
Compruebe siempre si el sitio tiene una API o renderizado lado servidor antes de llegar a un navegador sin cabeza. Muchos sitios "JavaScript-heavy" realmente tienen puntos finales de API que devuelven JSON limpios — mucho más rápido y más barato para raspar.
Puppeteer + Proxies (Node.js)
Controles de Puppeteer Chrome / Cromo programáticamente. Es la herramienta más madura del navegador sin cabeza para Node.js.
Configuración básica con ProxyHat
const puppeteer = require('puppeteer');
async function scrapeWithPuppeteer(url) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
],
});
const page = await browser.newPage();
// Authenticate with proxy
await page.authenticate({
username: 'USERNAME',
password: 'PASSWORD',
});
// Set realistic viewport and user agent
await page.setViewport({ width: 1920, height: 1080 });
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
// Wait for specific content to render
await page.waitForSelector('.product-list', { timeout: 10000 });
const content = await page.content();
const data = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.product-item')).map(el => ({
name: el.querySelector('.product-name')?.textContent?.trim(),
price: el.querySelector('.product-price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}));
});
return { html: content, data };
} finally {
await browser.close();
}
}
// Usage
const result = await scrapeWithPuppeteer('https://example.com/products');
console.log(`Found ${result.data.length} products`);Raspado multipágina optimizado
const puppeteer = require('puppeteer');
class PuppeteerScraper {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.browser = null;
}
async init() {
this.browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-extensions',
],
});
}
async scrapePage(url) {
const page = await this.browser.newPage();
await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
await page.setViewport({ width: 1920, height: 1080 });
// Block unnecessary resources to speed up loading
await page.setRequestInterception(true);
page.on('request', (req) => {
const type = req.resourceType();
if (['image', 'stylesheet', 'font', 'media'].includes(type)) {
req.abort();
} else {
req.continue();
}
});
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const content = await page.content();
return { url, status: 'success', html: content };
} catch (err) {
return { url, status: 'error', error: err.message };
} finally {
await page.close();
}
}
async scrapeMany(urls) {
const results = [];
for (let i = 0; i < urls.length; i += this.concurrency) {
const batch = urls.slice(i, i + this.concurrency);
const batchResults = await Promise.all(
batch.map(url => this.scrapePage(url))
);
results.push(...batchResults);
console.log(`Progress: ${results.length}/${urls.length}`);
}
return results;
}
async close() {
if (this.browser) await this.browser.close();
}
}
// Usage
const scraper = new PuppeteerScraper(3);
await scraper.init();
const results = await scraper.scrapeMany(urls);
await scraper.close();Playwright + Proxies (Python)
Playwright es una alternativa más nueva que soporta Chromium, Firefox y WebKit. Su API de Python es limpia y bien adaptada para raspar.
Configuración básica
from playwright.sync_api import sync_playwright
def scrape_with_playwright(url: str) -> dict:
"""Scrape a JavaScript-heavy page using Playwright with ProxyHat proxy."""
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME",
"password": "PASSWORD",
}
)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
)
page = context.new_page()
try:
page.goto(url, wait_until="networkidle", timeout=60000)
# Wait for dynamic content
page.wait_for_selector(".product-list", timeout=10000)
# Extract data using page.evaluate
products = page.evaluate("""() => {
return Array.from(document.querySelectorAll('.product-item')).map(el => ({
name: el.querySelector('.product-name')?.textContent?.trim(),
price: el.querySelector('.product-price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}));
}""")
return {"url": url, "products": products, "html": page.content()}
finally:
browser.close()Async Playwright for Parallel Scraping
import asyncio
from playwright.async_api import async_playwright
async def scrape_batch(urls: list[str], concurrency: int = 3) -> list[dict]:
"""Scrape multiple JS-heavy pages in parallel using Playwright."""
results = []
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME",
"password": "PASSWORD",
}
)
semaphore = asyncio.Semaphore(concurrency)
async def scrape_one(url: str) -> dict:
async with semaphore:
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
)
page = await context.new_page()
# Block heavy resources
await page.route("**/*.{png,jpg,jpeg,gif,svg,css,woff,woff2}",
lambda route: route.abort())
try:
await page.goto(url, wait_until="networkidle", timeout=30000)
html = await page.content()
return {"url": url, "status": "success", "html": html}
except Exception as e:
return {"url": url, "status": "error", "error": str(e)}
finally:
await context.close()
tasks = [scrape_one(url) for url in urls]
results = await asyncio.gather(*tasks)
await browser.close()
return results
# Usage
urls = [f"https://example.com/product/{i}" for i in range(50)]
results = asyncio.run(scrape_batch(urls, concurrency=5))Go: Usando cromado con Proxies
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/chromedp/chromedp"
)
func scrapeJSPage(targetURL string) (string, error) {
// Configure proxy
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.ProxyServer("http://gate.proxyhat.com:8080"),
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
var htmlContent string
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.WaitVisible(".product-list", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent),
)
if err != nil {
return "", fmt.Errorf("scrape failed: %w", err)
}
return htmlContent, nil
}
func main() {
html, err := scrapeJSPage("https://example.com/products")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Got %d bytes of rendered HTML\n", len(html))
}Estrategias de optimización del rendimiento
Los navegadores sin cabeza son 10-50x más lento que las simples peticiones HTTP. Aquí están las estrategias para minimizar la brecha de rendimiento:
1. Bloquear los recursos innecesarios
Imágenes, CSS, fuentes y archivos multimedia no son necesarios para la extracción de datos. Bloquearlas rápidamente cargas de página:
# Playwright resource blocking
async def fast_scrape(page, url):
# Block images, CSS, fonts, media
await page.route("**/*.{png,jpg,jpeg,gif,svg,css,woff,woff2,mp4,webm}",
lambda route: route.abort())
# Also block tracking scripts
await page.route("**/*google-analytics*", lambda route: route.abort())
await page.route("**/*facebook*", lambda route: route.abort())
await page.goto(url, wait_until="domcontentloaded") # Faster than networkidle
return await page.content()2. Use the Right Wait Strategy
| Estrategia | Speed | Confiabilidad | Caso de uso |
|---|---|---|---|
domcontentloaded | Rápido | May miss async data | Páginas con datos en línea |
load | Mediana | Bien. | Más páginas |
networkidle | Despacio. | Más alto | Espacios pesados, pergamino infinito |
| Selector específico | Variable | Más alto | Cuando usted conoce el elemento objetivo |
3. Instancias de navegador de reutilización
Lanzamiento de un navegador lleva 1-3 segundos. Para el raspado por lotes, lanzar una vez y crear nuevas páginas/contextos para cada URL:
from playwright.sync_api import sync_playwright
class BrowserPool:
"""Reusable browser pool for efficient headless scraping."""
def __init__(self, pool_size: int = 3):
self.pool_size = pool_size
self.playwright = None
self.browsers = []
def start(self):
self.playwright = sync_playwright().start()
for _ in range(self.pool_size):
browser = self.playwright.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME",
"password": "PASSWORD",
}
)
self.browsers.append(browser)
def get_browser(self, index: int):
return self.browsers[index % self.pool_size]
def stop(self):
for browser in self.browsers:
browser.close()
self.playwright.stop()
# Usage
pool = BrowserPool(pool_size=3)
pool.start()
for i, url in enumerate(urls):
browser = pool.get_browser(i)
context = browser.new_context()
page = context.new_page()
page.goto(url, wait_until="networkidle")
html = page.content()
context.close()
pool.stop()4. Interceptar llamadas de API en lugar de Parsing DOM
Muchos SPAs recogen datos de API. Interceptar esas llamadas de API directamente — se limpia JSON sin analizar HTML:
const puppeteer = require('puppeteer');
async function interceptAPIData(url) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--proxy-server=http://gate.proxyhat.com:8080'],
});
const page = await browser.newPage();
await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
const apiResponses = [];
// Intercept XHR/fetch responses
page.on('response', async (response) => {
const url = response.url();
if (url.includes('/api/') || url.includes('/graphql')) {
try {
const json = await response.json();
apiResponses.push({ url, data: json });
} catch {
// Not JSON, skip
}
}
});
await page.goto(url, { waitUntil: 'networkidle2' });
await browser.close();
return apiResponses;
}
// Get clean API data instead of scraping DOM
const data = await interceptAPIData('https://example.com/products');
console.log(`Intercepted ${data.length} API calls`);Navegador sin cabeza vs HTTP comparación
| métrica | HTTP simple + Proxy | Navegador sin cabeza + Proxy |
|---|---|---|
| Velocidad por página | 0,5-2 segundos | 3-15 segundos |
| Memoria por ejemplo | ~50 MB | 200 a 500 MB |
| Uso de CPU | Minimal | Significado |
| Ancho de banda por página | 50-200 KB | 2 a 10 MB (con recursos) |
| Reproducción de JavaScript | No | Total |
| Antibot bypass | Limited | Mejor (Navegador real) |
| Páginas concurrentes | 100+ | 3-10 por máquina |
Buenas prácticas
- Siempre prueba HTTP primero. Compruebe los puntos finales de API, el contenido de servidor o JSON incrustado en el HTML antes de utilizar un navegador sin cabeza.
- Bloquear recursos innecesarios. Imágenes, CSS y fuentes agregan tiempo de carga sin proporcionar datos.
- Use selectores específicos para esperar.
networkidlees seguro pero lento. Espera el elemento específico que necesitas. - Reutilizar las instancias del navegador. Inicie una vez, cree nuevos contextos por página.
- Interceptar llamadas API. Muchos SPAs cargan datos a través de APIs — interceptan directamente al JSON.
- Limite la concurrencia. Los navegadores sin cabeza son intensivos en memoria. 3-5 páginas concurrentes por GB de RAM es una buena regla.
- Use proxies residenciales. ProxyHat proxies residenciales proporcionar los puntajes de confianza más altos, reduciendo la detección cuando se ejecutan navegadores sin cabeza.
Para manejar CAPTCHAs que los navegadores sin cabeza encuentran, vea Handling CAPTCHAs Cuando Scraping. Para escalar el navegador sin cabeza raspando, leer Cómo escalar infraestructura de cambio.
Empieza con el Python SDK, Nodo SDKo Go SDK para la integración proxy, y explorar ProxyHat for Web Scraping.
Preguntas frecuentes
¿Siempre necesito un navegador sin cabeza para sitios JavaScript?
No. Muchos sitios con JavaScript cargan datos desde los puntos finales de API. Compruebe la pestaña Red del navegador para las solicitudes XHR/fetch — si los datos provienen de una API, puede llamar a esa API directamente con simples peticiones HTTP a través de un proxy, que es mucho más rápido.
Puppeteer o Playwright - ¿Qué es mejor para raspar?
Playwright generalmente se recomienda para nuevos proyectos. Soporta varios motores del navegador (Chromium, Firefox, WebKit), tiene un mejor soporte de asinc autóctono en Python y configuración de proxy integrada. Puppeteer es más maduro y tiene un ecosistema más grande si usted está en el mundo Node.js.
¿Cuántas páginas de navegador sin cabeza puedo ejecutar simultáneamente?
Cada página consume 200-500 MB de RAM. En una máquina con 8 GB de RAM, 3-10 páginas concurrentes es realista. Use bloqueo de recursos (imagenes, CSS) para reducir la memoria. Para mayor concurrencia, distribuya a través de múltiples máquinas utilizando una arquitectura basada en cola.
¿Por qué utilizar proxies con navegadores sin cabeza?
Incluso con un navegador real, las solicitudes repetidas de la misma IP se bloquean. Los ejes rotan su IP para que cada carga de página aparezca proveniente de un usuario diferente. Los proxies residenciales a través de ProxyHat proporcionan los puntajes de confianza más altos, minimizando bloques y CAPTCHAs.






