Despliegue

Capturas de pantalla en navegador headless: mejores prácticas y consejos

Cómo capturar capturas de pantalla consistentes y de alta calidad en modo headless, cubriendo viewport, DPI, formatos, temporización y captura de página completa.

Documentación

Prefieres la documentación del producto mantenida?

Este artículo tiene una página equivalente en el centro de documentación. Usa los docs para el flujo canónico, las flags actuales y la referencia duradera.

Introducción

Las capturas de pantalla en entornos de navegador headless son engañosamente complejas. Una captura que se ve perfecta en desarrollo puede producir imágenes en blanco, dimensiones incorrectas, fuentes faltantes o colores incorrectos en producción. Las causas raíz van desde la mala configuración del viewport y la temporización prematura de captura hasta problemas de configuración del servidor de pantalla y disponibilidad de fuentes.

BotBrowser añade otra dimensión a la captura de pantallas: la consistencia de la huella digital. El perfil define la resolución de pantalla, la proporción de píxeles del dispositivo y las dimensiones de la ventana. Si tu configuración de captura entra en conflicto con estos valores, obtienes una salida incorrecta o inconsistencias en la huella digital. Esta guía cubre el flujo completo de capturas de pantalla, desde la configuración de pantalla hasta la temporización de captura y la selección de formato, asegurando tanto la calidad visual como la coherencia de la huella digital.

Por qué importan las mejores prácticas de capturas

Las capturas de pantalla sirven para muchos propósitos en la automatización del navegador: pruebas de regresión visual, archivado de contenido, monitoreo de páginas y extracción de datos de contenido renderizado visualmente. Cada caso de uso tiene diferentes requisitos de calidad, pero todos comparten modos de fallo comunes.

El problema más frecuente es la temporización. Una captura tomada antes de que la página se cargue completamente produce una imagen en blanco o parcialmente renderizada. Las páginas con mucho JavaScript pueden tardar varios segundos después del evento de carga inicial antes de que todo el contenido sea visible. La carga de fuentes añade un retraso adicional, y las fuentes personalizadas que no se cargan producen texto de respaldo que no se parece en nada al diseño previsto.

Los desajustes de viewport y resolución son el segundo problema más común. Una captura tomada con el tamaño de viewport incorrecto captura demasiado o muy poco contenido. La proporción de píxeles del dispositivo afecta la resolución de salida: un viewport de 1920x1080 a 2x DPI produce una imagen de 3840x2160 píxeles, lo cual puede no ser lo que esperas.

En entornos headless, la configuración de pantalla de Xvfb afecta directamente el renderizado. Una pantalla configurada a 16 bits de profundidad de color produce bandas de color visibles. Una resolución de pantalla menor que el viewport recorta el contenido renderizado.

Contexto técnico

Cómo funcionan las capturas en Chrome headless

Cuando solicitas una captura a través de Playwright, Puppeteer o CDP, sucede lo siguiente:

  1. El compositor del navegador renderiza la página en un buffer fuera de pantalla.
  2. Los datos de píxeles se capturan de este buffer.
  3. Los datos se codifican al formato solicitado (PNG o JPEG).
  4. La imagen codificada se devuelve como una cadena base64 o se escribe en disco.

Las dimensiones del viewport determinan el tamaño del buffer de composición. La proporción de píxeles del dispositivo determina el factor de escala. Un viewport de 1920x1080 a 1x DPI produce una imagen de 1920x1080. El mismo viewport a 2x DPI produce una imagen de 3840x2160 con texto y gráficos más nítidos.

Perfil de BotBrowser y capturas

Los perfiles de BotBrowser definen screen.width, screen.height, window.innerWidth, window.innerHeight y devicePixelRatio. Estos valores afectan lo que JavaScript reporta cuando una página consulta las propiedades de pantalla, pero también influyen en cómo se distribuye y renderiza el contenido.

Para huellas digitales consistentes, el viewport de renderizado real debe coincidir con los valores del perfil. Establecer defaultViewport: null en Puppeteer indica al framework que no anule estos valores. En Playwright, el contexto del navegador respeta los valores del perfil por defecto cuando no se establece un viewport explícitamente.

Xvfb y calidad de renderizado

En servidores Linux headless, Xvfb proporciona la pantalla X11 que Chrome usa para la inicialización del renderizado. La configuración de Xvfb afecta directamente la calidad de la salida:

  • Resolución: Debe ser al menos tan grande como el viewport más grande que uses. 1920x1080 cubre la mayoría de los perfiles de escritorio. 2560x1440 proporciona margen para viewports más grandes.
  • Profundidad de color: Debe ser de 24 bits (x24). Profundidades menores causan bandas de color y afectan la salida de huella digital de Canvas.
  • Número de pantalla: Cualquier número no utilizado funciona. :10 es convencional para configuraciones de BotBrowser.

<svg viewBox="0 0 700 260" xmlns="http://www.w3.org/2000/svg" style={{maxWidth: '100%', height: 'auto'}}> Screenshot Pipeline Navigate Load Page Wait Content Ready Fonts document.fonts Capture PNG / JPEG Common Timing Failures: Too early: blank or partial page. Missing images, unstyled content. Font not ready: fallback fonts render, text layout shifts after screenshot.

Enfoques comunes y limitaciones

Captura inmediata después de la navegación

// Problemático: captura antes de que el contenido se cargue
await page.goto('https://example.com');
await page.screenshot({ path: 'output.png' });

Esto captura lo que esté en pantalla en el momento en que la promesa de goto se resuelve con el evento load predeterminado. Para muchas páginas, contenido significativo se carga de forma asíncrona después de este evento. El resultado es a menudo una página parcialmente renderizada.

Retraso fijo

// Frágil: puede ser demasiado corto o demasiado largo
await page.goto('https://example.com');
await new Promise(r => setTimeout(r, 3000));
await page.screenshot({ path: 'output.png' });

Añadir un retraso fijo no es confiable. Tres segundos pueden no ser suficientes para páginas lentas y es un desperdicio para las rápidas. Las condiciones de red, los tiempos de respuesta del servidor y la complejidad de la página varían.

Solo networkidle0

// Mejor pero no perfecto
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'output.png' });

La estrategia networkidle0 espera hasta que no haya más de 0 conexiones de red durante 500ms. Esto maneja la mayoría de la carga de contenido asíncrono pero no detecta el renderizado solo por JavaScript que no involucra solicitudes de red, y puede quedarse colgado en páginas con conexiones WebSocket persistentes o solicitudes de polling.

Ignorar la configuración del viewport

Depender del viewport predeterminado (generalmente 800x600 en Puppeteer) en lugar de configurarlo para coincidir con el perfil produce capturas con dimensiones incorrectas. El contenido se reorganiza para ajustarse al viewport más pequeño, produciendo un diseño similar al móvil en un perfil de escritorio.

El enfoque de BotBrowser

Los perfiles de BotBrowser incluyen datos de viewport y dimensiones de pantalla que representan una configuración de dispositivo real. El enfoque recomendado es dejar que el perfil controle las dimensiones del viewport en lugar de establecerlas manualmente. Esto asegura que la captura represente lo que un usuario real con ese dispositivo vería.

Preservar las dimensiones del perfil

Puppeteer: Establece defaultViewport: null para evitar que Puppeteer anule el viewport:

const browser = await puppeteer.launch({
  executablePath: '/opt/botbrowser/chrome',
  args: [
    '--bot-profile=/opt/profiles/profile.enc',
    '--window-size=1920,1080',
  ],
  headless: true,
  defaultViewport: null,
});

Playwright: El contexto del navegador hereda las dimensiones del perfil por defecto. Si necesitas anular, hazlo explícitamente:

const browser = await chromium.launch({
  executablePath: '/opt/botbrowser/chrome',
  args: [
    '--bot-profile=/opt/profiles/profile.enc',
  ],
  headless: true,
});

const context = await browser.newContext();
// Las dimensiones del viewport del perfil se preservan

Dimensiones de ventana personalizadas

Cuando necesitas un viewport específico independientemente del perfil:

chromium-browser \
  --bot-profile="/opt/profiles/profile.enc" \
  --bot-config-window="1920x1080" \
  --bot-config-screen="2560x1440" \
  --window-size=1920,1080 \
  --headless

El flag --bot-config-window actualiza lo que reportan las APIs JavaScript. El flag --window-size establece el viewport real de Chromium.

Configuración y uso

Espera confiable de carga de página

Combina múltiples estrategias de espera para máxima confiabilidad:

async function waitForPageReady(page, url) {
  // Navegar con networkidle0 para la mayoría del contenido
  await page.goto(url, {
    waitUntil: 'networkidle0',
    timeout: 30000,
  });

  // Esperar un elemento de contenido específico si se conoce
  try {
    await page.waitForSelector('.main-content', {
      visible: true,
      timeout: 5000,
    });
  } catch (e) {
    // El elemento puede no existir en todas las páginas
  }

  // Esperar a que las fuentes terminen de cargarse
  await page.evaluate(() => document.fonts.ready);

  // Opcional: esperar imágenes con carga diferida
  await page.evaluate(async () => {
    const images = document.querySelectorAll('img[loading="lazy"]');
    await Promise.all(
      Array.from(images)
        .filter(img => !img.complete)
        .map(img => new Promise(resolve => {
          img.onload = resolve;
          img.onerror = resolve;
        }))
    );
  });
}

Capturas de página completa

Captura la página completa, incluyendo el contenido debajo del pliegue:

await page.screenshot({
  path: 'fullpage.png',
  fullPage: true,
  type: 'png',
});

Nota: las capturas de página completa pueden ser muy grandes para páginas largas. Una página de 1920 píxeles de ancho que se desplaza hasta 10,000 píxeles produce una imagen de 19,200,000 píxeles a 1x DPI. Considera capturar solo el viewport visible para monitoreo de rendimiento.

Capturas específicas de elementos

Captura un elemento específico sin la página circundante:

const element = await page.$('.target-element');
if (element) {
  await element.screenshot({
    path: 'element.png',
    type: 'png',
  });
}

Capturas de alto DPI

Para capturas con calidad retina, usa un perfil con proporción de píxeles del dispositivo de 2x, o anúlalo:

// Playwright: establecer factor de escala del dispositivo en el contexto
const context = await browser.newContext({
  deviceScaleFactor: 2,
});

El PNG resultante tendrá el doble de las dimensiones del viewport en cada dirección.

Selección de formato

PNG es sin pérdida y produce una salida perfecta a nivel de píxel. Úsalo para:

  • Pruebas de regresión visual donde la comparación exacta de píxeles importa
  • Archivado donde la calidad no puede comprometerse
  • Imágenes con texto, líneas o bordes nítidos

JPEG tiene pérdida pero es mucho más pequeño. Úsalo para:

  • Captura de alto volumen donde el almacenamiento importa
  • Miniaturas o vistas previas donde la calidad perfecta no es necesaria
  • Fotografías o imágenes complejas donde los artefactos de compresión son menos visibles
// PNG para calidad
await page.screenshot({ path: 'output.png', type: 'png' });

// JPEG para tamaño (calidad 0-100)
await page.screenshot({ path: 'output.jpg', type: 'jpeg', quality: 85 });

Comparación entre viewport y página completa

AspectoCaptura de viewportCaptura de página completa
TamañoFijo, predecibleVariable, puede ser muy grande
VelocidadRápidaMás lenta para páginas largas
ContenidoSolo visible sin desplazarContenido completo de la página
Caso de usoMonitoreo, comparaciónArchivado, extracción de contenido

Función de captura para producción

const puppeteer = require('puppeteer-core');

async function captureScreenshot(url, outputPath, options = {}) {
  const {
    format = 'png',
    quality = 85,
    fullPage = false,
    waitSelector = null,
    timeout = 30000,
  } = options;

  const browser = await puppeteer.launch({
    executablePath: '/opt/botbrowser/chrome',
    args: [
      '--bot-profile=/opt/profiles/profile.enc',
      '--window-size=1920,1080',
    ],
    headless: true,
    defaultViewport: null,
  });

  try {
    const page = await browser.newPage();

    // Navegar y esperar contenido
    await page.goto(url, {
      waitUntil: 'networkidle0',
      timeout: timeout,
    });

    // Esperar elemento específico si se proporciona
    if (waitSelector) {
      await page.waitForSelector(waitSelector, {
        visible: true,
        timeout: 10000,
      });
    }

    // Esperar fuentes
    await page.evaluate(() => document.fonts.ready);

    // Capturar
    const screenshotOptions = {
      path: outputPath,
      type: format,
      fullPage: fullPage,
    };
    if (format === 'jpeg') {
      screenshotOptions.quality = quality;
    }

    await page.screenshot(screenshotOptions);
    console.log(`Screenshot saved: ${outputPath}`);
  } finally {
    await browser.close();
  }
}

// Ejemplos de uso
captureScreenshot('https://example.com', '/output/example.png');

captureScreenshot('https://example.com', '/output/example-full.jpg', {
  format: 'jpeg',
  quality: 90,
  fullPage: true,
  waitSelector: '.main-content',
});

Captura por lotes

Para capturar muchas páginas eficientemente:

async function batchCapture(urls, outputDir) {
  const browser = await puppeteer.launch({
    executablePath: '/opt/botbrowser/chrome',
    args: [
      '--bot-profile=/opt/profiles/profile.enc',
      '--window-size=1920,1080',
    ],
    headless: true,
    defaultViewport: null,
  });

  try {
    for (let i = 0; i < urls.length; i++) {
      const page = await browser.newPage();
      try {
        await page.goto(urls[i], {
          waitUntil: 'networkidle0',
          timeout: 20000,
        });
        await page.evaluate(() => document.fonts.ready);
        await page.screenshot({
          path: `${outputDir}/page-${i}.png`,
          type: 'png',
        });
      } catch (err) {
        console.error(`Failed: ${urls[i]}`, err.message);
      } finally {
        await page.close();  // Liberar memoria entre capturas
      }
    }
  } finally {
    await browser.close();
  }
}

Configuración de Xvfb para capturas

Haz coincidir la resolución de Xvfb con tu viewport esperado más grande:

# Perfiles estándar de 1080p
Xvfb :10 -screen 0 1920x1080x24 &

# Perfiles de 1440p o alto DPI
Xvfb :10 -screen 0 2560x1440x24 &

# Margen extra para capturas de página completa
Xvfb :10 -screen 0 3840x2160x24 &

Siempre usa profundidad de color de 24 bits. Establece DISPLAY=:10.0 antes de lanzar BotBrowser.

Verificación

Verifica la calidad de las capturas con una prueba:

DISPLAY=:10.0 node -e "
const puppeteer = require('puppeteer-core');
(async () => {
  const b = await puppeteer.launch({
    executablePath: '/opt/botbrowser/chrome',
    args: ['--bot-profile=/opt/profiles/profile.enc', '--window-size=1920,1080'],
    headless: true,
    defaultViewport: null,
  });
  const p = await b.newPage();
  await p.goto('https://example.com', { waitUntil: 'networkidle0' });
  await p.evaluate(() => document.fonts.ready);
  await p.screenshot({ path: '/tmp/test-screenshot.png', type: 'png' });
  console.log('Screenshot saved to /tmp/test-screenshot.png');
  await b.close();
})();
"

Verifica el archivo de salida para:

  • Dimensiones correctas que coincidan con el viewport
  • Color completo (sin bandas ni artefactos de color)
  • Todo el texto renderizado con las fuentes apropiadas
  • Contenido completo de la página (sin secciones faltantes)

Mejores prácticas

Siempre establece defaultViewport: null en Puppeteer. Esto preserva las dimensiones del viewport del perfil. Sin ello, Puppeteer usa por defecto 800x600.

Espera las fuentes antes de capturar. await page.evaluate(() => document.fonts.ready) previene el renderizado con fuentes de respaldo.

Usa networkidle0 para la carga inicial, luego espera elementos específicos. Este enfoque de dos etapas maneja la mayoría de las páginas de forma confiable.

Cierra las páginas entre capturas en operaciones por lotes. Esto previene la acumulación de memoria que degrada la calidad de las capturas posteriores.

Usa PNG para precisión, JPEG para volumen. PNG es sin pérdida pero más grande. JPEG a calidad 85-90 es un buen equilibrio para la mayoría de los casos de uso.

Haz coincidir la resolución de Xvfb con tu viewport. Una pantalla Xvfb más pequeña que tu viewport recorta el área de renderizado.

Usa profundidad de color de 24 bits. Siempre 1920x1080x24, nunca 1920x1080x16 o 1920x1080x8.

Preguntas frecuentes

¿Por qué mis capturas están en blanco?

La causa más común es capturar antes de que la página termine de cargarse. Usa waitUntil: 'networkidle0' y añade esperas explícitas para elementos de contenido clave. Otra causa es una pantalla Xvfb faltante o mal configurada en Linux.

¿Por qué la captura tiene el tamaño incorrecto?

En Puppeteer, el viewport predeterminado es 800x600. Establece defaultViewport: null para usar el tamaño real de la ventana. Para BotBrowser, también verifica que --window-size o --bot-config-window coincidan con las dimensiones esperadas.

¿Cómo capturo un elemento detrás de un modal?

Cierra u oculta el modal primero usando page.evaluate(), luego captura el elemento. Alternativamente, captura la página completa y recorta al cuadro delimitador del elemento.

¿La proporción de píxeles del dispositivo afecta el tamaño de la captura?

Sí. Un viewport de 1920x1080 a 2x DPI produce una imagen de 3840x2160 píxeles. Este es el comportamiento correcto para capturas de alto DPI. Si quieres una salida de 1920x1080 píxeles, usa 1x DPI.

¿Puedo capturar con media queries CSS específicas?

Sí. Usa page.emulateMediaType('print') para diseño de impresión, o inyecta CSS mediante page.addStyleTag() para condiciones de medios personalizadas.

¿Cómo manejo páginas con scroll infinito?

Desplázate a la posición deseada primero usando page.evaluate(() => window.scrollTo(0, 5000)), espera a que se cargue el contenido, luego captura. Las capturas de página completa en páginas con scroll infinito solo capturarán el contenido que se haya cargado.

¿Cuál es el tamaño máximo de captura?

Chrome limita la superficie de renderizado. Las páginas muy largas (por encima de ~16,000 píxeles de alto) pueden producir capturas truncadas. Para páginas extremadamente largas, captura secciones y únalas.

¿Cómo comparo capturas para pruebas de regresión visual?

Usa herramientas como pixelmatch (paquete npm) o resemblejs para comparar dos archivos PNG píxel por píxel. Establece un umbral de tolerancia para tener en cuenta diferencias menores de anti-aliasing entre ejecuciones.

Resumen

La captura de pantallas confiable en entornos headless requiere atención a la configuración del viewport, la temporización de carga de la página, la preparación de las fuentes y la configuración del servidor de pantalla. Con BotBrowser, los valores de viewport y DPI del perfil deben guiar tu configuración. Usa defaultViewport: null en Puppeteer, espera tanto la inactividad de red como la carga de fuentes antes de capturar, y siempre usa profundidad de color de 24 bits en Xvfb.

Para la configuración de pantalla, consulta Configuración de servidor headless. Para pipelines de captura basados en Docker, consulta la Guía de despliegue con Docker. Para optimizar el volumen de capturas a escala, consulta Optimizar el rendimiento de BotBrowser.

#Screenshot#headless#despliegue#Best-Practices#Production

Lleva BotBrowser de la investigación a producción

Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.