Desarrollo26 de junio de 2026

Capturas de pantalla automáticas de páginas web con Puppeteer y Sharp

Hace poco estuve trabajando en una tarea de automatización para uno de nuestros servicios internos: hacer capturas de página completa con calidad retina de una web. Debería haber sido un problema trivial, pero, por lo visto, en 2022 todavía no hay ninguna librería de testing o automatización de navegadores que lo resuelva.

Probé Playwright, Puppeteer y Selenium, y todas tienen los mismos problemas: las capturas de página completa hechas con la API estándar o incluyen secciones en blanco (las zonas de la web donde está el contenido dinámico), o el proceso entero falla si la página es lo bastante larga, como en mi caso, con un viewport de 1920px de ancho y una altura de más de 10.000px.

Este es el ejemplo de mi primer enfoque, directo al grano, con una llamada genérica a la API de Puppeteer:


const puppeteer = require('puppeteer');
const url = 'https://stripe.com/';
const screenshot = async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({
width: 1920,
height: 1080,
deviceScaleFactor: 2,
});
await page.goto(url);
await page.screenshot({
path: 'screenshot.png',
fullPage: true,
});
await browser.close();
};
screenshot();

Que, en ciertos casos, da como resultado esto:

Ejemplo de captura hecha con la API por defecto
Ejemplo de captura hecha con la API por defecto

Después de darle vueltas durante unas horas, encontré una solución alternativa: hacer varias capturas de la página y luego unirlas. En este artículo te enseño cómo hacerlo con Puppeteer y la librería Sharp.

Empecemos instalando Puppeteer y Sharp:


npm install puppeteer sharp

Después, declara unas cuantas constantes; las vamos a necesitar más adelante.


const puppeteer = require('puppeteer');
const fs = require('fs');
const sharp = require('sharp');
// URL of the webpage you want to take screenshot of
const URL = 'https://tailwindcss.com/';
// viewport configuration
const VIEWPORT_WIDTH = 1920;
const VIEWPORT_HEIGHT = 1080;
const DEVICE_SCALE_FACTOR = 2;
// Chunk height is a value in px that defines
// maximum height of each screenshot.
// Use a smaller or bigger number based on the tests.
const CHUNK_HEIGHT = 6000;

Para calcular correctamente cuántas capturas hay que hacer, necesitamos conocer la altura exacta de la página. Para ello usaremos esta función:


const getHeight = async (page) => {
const height = await page.evaluate(() => {
return document.body.scrollHeight;
});
return height;
};

Y otra función auxiliar más, que nos permitirá hacer scroll automático hasta el final de la página para disparar todas las animaciones y la carga de imágenes antes de hacer la captura.


async function autoScroll(page) {
try {
await page.evaluate(async () => {
// Scroll to the bottom of the page, so that all the content is visible and loaded
await new Promise((resolve, reject) => {
var totalHeight = 0;
var distance = 100;
var timer = setInterval(() => {
var scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 50);
});
// Scroll back to the top of the page, so sticky menus will appear at the top
document.body.scrollTop = document.documentElement.scrollTop = 0;
});
} catch (err) {
console.log(err);
}
}

Lo último es hacer la captura. Aquí tienes el fragmento de código completo, con comentarios adicionales que te pueden ayudar a conseguir el mismo resultado.


const puppeteer = require('puppeteer');
const fs = require('fs');
const sharp = require('sharp');
// Puppeteer configuration options
const URL = 'https://tailwindcss.com/';
const VIEWPORT_WIDTH = 1920;
const VIEWPORT_HEIGHT = 1080;
const DEVICE_SCALE_FACTOR = 2;
const CHUNK_HEIGHT = 6000;
// Helper function, needed for calculating number of chunks
const getHeight = async (page) => {
const height = await page.evaluate(() => {
return document.body.scrollHeight;
});
return height;
};
// Auto scroll the page to trigger all page animations and image loading
async function autoScroll(page) {
try {
await page.evaluate(async () => {
await new Promise((resolve, reject) => {
var totalHeight = 0;
var distance = 100;
var timer = setInterval(() => {
var scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 50);
});
// Scroll back to the top of the page, so sticky menus appears at the right place.
document.body.scrollTop = document.documentElement.scrollTop = 0;
});
} catch (err) {
console.log(err);
}
}
const screenshot = async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({
width: VIEWPORT_WIDTH,
height: VIEWPORT_HEIGHT,
deviceScaleFactor: DEVICE_SCALE_FACTOR,
});
await page.goto(URL);
await autoScroll(page);
const pageHeight = await getHeight(page);
await page.waitForNetworkIdle();
// If page has height more than 6000px, then split it into the chunks and take screenshots of each chunk
if (pageHeight > CHUNK_HEIGHT) {
const chunks = Math.ceil(pageHeight / CHUNK_HEIGHT);
const chunkCompositeOptions = [];
for (let i = 0; i < chunks; i++) {
let height = 0;
// Calculate chunk offset top for a screenshot
const offsetTop = i * CHUNK_HEIGHT;
const filePath = `./screenshot-${i}.png`;
if (pageHeight - CHUNK_HEIGHT * i < CHUNK_HEIGHT) {
height = pageHeight - CHUNK_HEIGHT * i;
// Take a screenshot using clip option passing top offset
await page.screenshot({
path: filePath,
clip: {
x: 0,
y: offsetTop,
width: VIEWPORT_WIDTH,
height: pageHeight - CHUNK_HEIGHT * i,
},
});
} else {
height = CHUNK_HEIGHT;
await page.screenshot({
path: filePath,
clip: {
x: 0,
y: offsetTop,
width: VIEWPORT_WIDTH,
height: CHUNK_HEIGHT,
},
});
}
// Prepare image merge options for sharp library api
chunkCompositeOptions.push({
input: filePath,
top: offsetTop * DEVICE_SCALE_FACTOR,
left: 0,
width: VIEWPORT_WIDTH,
height: height * DEVICE_SCALE_FACTOR,
});
}
// An example to find batch of files in the folder
const files = fs.readdirSync('./');
const images = files.filter((file) => file.includes('screenshot'));
// Merge images into one
await sharp({
create: {
width: VIEWPORT_WIDTH * DEVICE_SCALE_FACTOR,
height: pageHeight * DEVICE_SCALE_FACTOR,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 0 },
},
})
.composite(chunkCompositeOptions)
.toFile('./merged.png');
// Delete all the screenshots
images.forEach((image) => fs.unlinkSync(image));
} else {
await page.screenshot({
path: './screenshot.png',
fullPage: true,
});
}
await browser.close();
};
screenshot();

¡Y listo! Guárdate este truco y úsalo para crear automáticamente capturas bonitas y de alta calidad de cualquier página web que quieras.

Cuéntanos tu caso

Escríbenos