Como automatizar capturas de tela de sites com Puppeteer
Use o Puppeteer para automatizar capturas de tela de sites com JavaScript. Capture uma página, páginas inteiras, lotes de URLs e viewports móveis com um script reutilizável.
13 min de leituraCaptura de sitePor Temposhot Team
Puppeteer é uma biblioteca Node.js mantida pela equipe do Chrome. Ela controla o Chrome ou o Firefox por meio de uma API em JavaScript e é uma escolha comum para capturas de tela de sites feitas por script. Os exemplos abaixo partem de uma captura de cinco linhas e chegam a um executor reutilizável que lê URLs de um arquivo, executa páginas em paralelo e grava um relatório.
A última seção mostra o mesmo lote como uma execução do Temposhot Website Capture em um Mac, sem script para manter. As duas abordagens produzem arquivos comparáveis. A escolha depende de onde a tarefa roda.
Quando o Puppeteer é a ferramenta certa
O Puppeteer se encaixa quando as capturas de tela fazem parte de um código que você já executa:
- um serviço Node.js que gera prévias ou imagens para redes sociais
- um cron job ou uma etapa de CI que captura páginas depois de um deploy
- um job de scraping que também precisa de um registro visual
- uma equipe que já usa recursos do Chrome DevTools Protocol
Ele é menos adequado para tarefas pontuais feitas por pessoas que não mantêm projetos Node.js.
Instale o Puppeteer para um script de captura
mkdir site-shots && cd site-shots
npm init -y
npm install puppeteer
O pacote puppeteer baixa uma build compatível do Chrome durante a instalação. O pacote mais leve puppeteer-core pula esse download e precisa do caminho até um navegador já instalado. Os scripts abaixo usam arquivos .mjs para que o await no nível superior funcione.
Escolha entre puppeteer e puppeteer-core
Use puppeteer em uma máquina de desenvolvedor em que o download é permitido. Use puppeteer-core em uma imagem de CI que já tem o Chrome instalado, ou em uma rede que bloqueia o download do navegador. A API é a mesma nos dois pacotes, então o script não muda. Só a chamada de launch precisa de executablePath quando o navegador fica em um local personalizado.
Capture a tela de um site
// one.mjs
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'example.png' });
await browser.close();
page.setViewport define a largura e a altura. Adicione deviceScaleFactor: 2 à mesma chamada para gerar uma imagem com o dobro da densidade de pixels, semelhante a uma tela Retina.
O que as configurações produzem
O script acima, com deviceScaleFactor: 2 adicionado, tem um tamanho de saída fixo. Os números decorrem do viewport e do fator de escala.
A saída tem o dobro do viewport em cada direção, porque o fator de escala multiplica os pixels e o tamanho em CSS continua o mesmo. Um viewport de 1440 por 900 com escala 1 gera uma imagem de 1440 por 900, que é o tamanho certo para uma prévia web. O fator de escala serve para obter uma imagem nítida em telas de alta densidade, e isso dobra o tamanho do arquivo.
Capture uma página inteira
await page.screenshot({ path: 'example-full.png', fullPage: true });
Com fullPage: true, a imagem cobre todo o documento rolável. Para um arquivo menor, salve em JPEG ou WebP:
await page.screenshot({ path: 'example.jpg', type: 'jpeg', quality: 80, fullPage: true });
quality aceita valores de 0 a 100 e se aplica apenas a JPEG e WebP. Uma captura de página inteira de uma página longa pode ter dezenas de milhares de pixels de altura, então o JPEG costuma manter o tamanho do arquivo sob controle.
Tamanho de uma página longa
A largura permanece igual à largura do viewport, e a altura acompanha o documento. Um artigo longo deixa a imagem muito alta. Os números abaixo mostram a dimensão dessa mudança.
Um quadro de 3840 por 2160 tem cerca de 8,3 milhões de pixels, então a imagem de exemplo tem quase o dobro do tamanho de um quadro 4K. Uma imagem alta é difícil de ver inteira. Divida páginas muito longas em seções com clip quando a imagem for exibida em uma tela ou em um documento.
Recorte uma seção
Use a opção clip para salvar uma região. As coordenadas são pixels CSS a partir do topo da página, não a partir do viewport:
await page.screenshot({
path: 'example-pricing.png',
clip: { x: 0, y: 2400, width: 1440, height: 900 },
});
Use fullPage ou clip, mas não os dois na mesma chamada. Para dividir uma página longa em vários arquivos, leia a altura com document.body.scrollHeight e faça um clip por fatia. Uma página de 12000 pixels dividida em fatias de 900 pixels gera 14 arquivos, e o último contém apenas 300 pixels.
Crie um executor reutilizável de URLs
// run.mjs
import { readFile, mkdir, writeFile } from 'node:fs/promises';
import puppeteer from 'puppeteer';
const urls = (await readFile('urls.txt', 'utf8'))
.split('\n').map((l) => l.trim()).filter(Boolean);
const outDir = `shots/${new Date().toISOString().slice(0, 10)}`;
await mkdir(outDir, { recursive: true });
const browser = await puppeteer.launch();
async function shoot(url, index) {
const page = await browser.newPage();
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
const path = `${outDir}/${String(index + 1).padStart(3, '0')}.jpg`;
await page.screenshot({ path, type: 'jpeg', quality: 80, fullPage: true });
return { url, path, ok: true };
} finally {
await page.close();
}
}
Uma página nova por URL significa que uma falha em um site não afeta o próximo. A pasta de saída com data mantém as execuções anteriores intactas.
O executor tem cinco etapas. A figura abaixo lista as etapas na ordem, com o arquivo ou processo que cada uma utiliza.
Limpe a lista de URLs
Linhas em branco, comentários e URLs duplicadas desperdiçam tempo do navegador, e uma duplicata gera um segundo arquivo para a mesma página. A versão abaixo ignora linhas que começam com #, remove duplicatas exatas e descarta qualquer item que não seja um endereço HTTP ou HTTPS. raw contém o conteúdo do arquivo lido na etapa anterior.
const lines = raw.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
const urls = [...new Set(lines)].filter((l) => {
try {
const { protocol } = new URL(l);
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
});
Set compara strings de forma exata, então https://example.com e https://example.com/ continuam como duas URLs. Registre também as linhas rejeitadas. Um erro de digitação como htps://example.com aparece no log logo no início da execução, em vez de virar um timeout 45 segundos depois.
Numeração para lotes grandes
O script completa o índice com três dígitos, o que ordena corretamente até 999 arquivos. O índice 1000 vira 1000.jpg, e uma ordenação textual o coloca entre 100.jpg e 101.jpg. Use a largura do lote:
const width = String(urls.length).length;
const path = `${outDir}/${String(index + 1).padStart(width, '0')}.jpg`;
Um lote de 1.250 URLs tem largura 4, então o primeiro arquivo é 0001.jpg. Calcule a largura uma vez, antes de os workers iniciarem.
Adicione concorrência para lotes maiores
const LIMIT = 4;
const queue = [...urls.entries()];
const results = [];
async function worker() {
while (queue.length) {
const [i, url] = queue.shift();
results.push(await shootWithRetry(url, i));
}
}
await Promise.all(Array.from({ length: LIMIT }, worker));
await browser.close();
Cada worker pega a próxima URL de uma fila compartilhada. Comece com 4 e aumente o limite só depois de verificar a memória, já que cada página aberta usa o próprio processo de renderização.
O script abre todas as páginas a partir de um único navegador. As páginas compartilham o cache do navegador e, por padrão, também compartilham cookies. Em um lote de sites públicos sem relação entre si, isso é inofensivo. Para uma execução que não pode carregar estado de um site para outro, crie um contexto por URL com browser.createBrowserContext() e feche-o depois da captura. Cada contexto consome memória, então essa escolha deve ser avaliada junto com o limite de concorrência.
Escolha o limite
O limite define quantas páginas são renderizadas ao mesmo tempo. Um limite baixo mantém a memória estável e deixa a execução mais longa, e um limite alto faz o contrário. A tabela abaixo mostra essa troca para um lote de 200 URLs, em que cada página leva cerca de 6 segundos em média.
Os tempos ideais assumem nenhuma espera de rede e nenhuma falha. Sites reais variam, e timeouts e novas tentativas somam tempo. Meça um lote de 20 URLs em dois limites na máquina de destino e escolha o limite mais alto que mantenha o Mac responsivo.
Capture telas em dispositivos móveis
O Puppeteer inclui descritores de dispositivos por meio de KnownDevices.
import puppeteer, { KnownDevices } from 'puppeteer';
const page = await browser.newPage();
await page.emulate(KnownDevices['iPhone 13']);
await page.goto('https://example.com');
page.emulate define o viewport, a razão de pixels, o suporte a toque e o user agent em uma única chamada. Para uma largura móvel simples, sem alterar o user agent, chame setViewport com isMobile: true e hasTouch: true.
Use emulate quando um site entrega marcações diferentes por user agent, e setViewport quando só importa a largura do layout. Ambas as chamadas se aplicam à página em que são executadas, então uma segunda página aberta depois precisa da sua própria chamada.
Um lote que mistura desktop e mobile precisa de um viewport por conjunto de saída, e não de um por script. Mantenha as imagens móveis em uma pasta com data própria, como shots/2026-04-10-mobile, para que os números dos arquivos dos dois conjuntos não colidam. O Temposhot Website Capture usa o tamanho Mobile de 390 por 844 para o mesmo fim, definido por linha na mesma execução.
Confirme o viewport emulado
Verifique a largura do layout na página antes da captura. Um descritor errado gera um layout de desktop dentro de uma imagem de tamanho móvel. O código abaixo informa a largura e a razão de pixels que a página vê:
const seen = await page.evaluate(() => ({
width: window.innerWidth,
pixelRatio: window.devicePixelRatio,
}));
console.log(seen);
Para o descritor iPhone 13 nas versões recentes do Puppeteer, a largura deve ser 390. A razão de pixels também vem do descritor. Confira os valores na versão que você instalar, já que os dados dos descritores mudam entre as versões.
Aguarde o conteúdo carregado sob demanda
Imagens carregadas ao rolar a página ficam em branco em uma captura feita logo após a navegação. Role a página antes:
await page.evaluate(async () => {
for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight) {
window.scrollTo(0, y);
await new Promise((r) => setTimeout(r, 200));
}
window.scrollTo(0, 0);
});
O glossário sobre lazy loading explica o padrão.
Quanto tempo leva a rolagem
O laço espera 200 milissegundos por passo, e cada passo avança a altura de um viewport. O exemplo abaixo é uma página de 8000 pixels com viewport de 900 pixels.
A página tem nove passos, então a passagem adiciona 1,8 segundo antes da captura de tela. Uma página que carrega imagens devagar precisa de uma pausa maior por passo, e uma página com rolagem infinita nunca chega a um fim fixo. Nesse caso, pare depois de um número fixo de passos, em vez de parar na altura do documento.
Role dentro de um contêiner
Alguns layouts rolam um elemento interno em vez da janela. O laço acima então não detecta movimento, e a parte inferior da página fica em branco. Role o elemento pela propriedade scrollTop, usando o próprio scrollHeight e clientHeight como limites. Depois da execução, compare o primeiro e o último quadro. Dois quadros idênticos significam que a página não rolou, o que indica um contêiner.
Escolha a estratégia de espera correta
| Opção | Retorna quando |
|---|---|
load | O evento load é disparado. Este é o padrão. |
domcontentloaded | O HTML é analisado. |
networkidle0 | Não há conexões de rede por pelo menos 500 ms. |
networkidle2 | Não há mais de 2 conexões por pelo menos 500 ms. |
networkidle0 pode dar timeout em páginas que fazem polling ou streaming. networkidle2 tolera esse tráfego em segundo plano. Quando a página tem um sinal claro de que está pronta, page.waitForSelector sobre esse elemento é a escolha mais confiável. O timeout padrão de navegação é de 30 segundos.
Escolha um sinal que a página defina somente quando o conteúdo estiver pronto. Um seletor que existe em todo carregamento, como o cabeçalho, não comprova nada sobre os dados abaixo dele.
Meça antes de escolher uma espera
Meça o tempo de navegação de cada URL antes de decidir a estratégia de espera. O código abaixo mede o tempo até o evento load ser disparado:
const started = Date.now();
await page.goto(url, { waitUntil: 'load', timeout: 45000 });
const loadMs = Date.now() - started;
Grave loadMs no relatório ao lado de cada URL. Se a maioria das páginas carrega em poucos segundos, a espera padrão é adequada. Se um grupo de páginas fica perto do timeout, troque essas URLs para domcontentloaded, espere por um seletor e mantenha o relatório para confirmar a mudança.
Adicione novas tentativas e um relatório
async function shootWithRetry(url, i, tries = 3) {
for (let attempt = 1; attempt <= tries; attempt++) {
try {
return await shoot(url, i);
} catch (error) {
if (attempt === tries) return { url, ok: false, error: error.message };
}
}
}
// after Promise.all in run.mjs
await writeFile(`${outDir}/report.json`, JSON.stringify(results, null, 2));
O relatório lista cada URL com o caminho do arquivo ou o erro. Envie as entradas com falha de volta para urls.txt para uma segunda passagem.
Monte a lista de novas tentativas a partir do relatório
O relatório transforma a segunda passagem em um filtro, em vez de uma cópia manual. Este comando jq grava as URLs com falha em um novo arquivo:
jq -r '.[] | select(.ok == false) | .url' shots/2026-04-10/report.json > urls-retry.txt
Aponte o script para urls-retry.txt na segunda passagem. O script nomeia a pasta de saída pela data, então uma segunda execução no mesmo dia grava na mesma pasta. Arquivos com os mesmos números sobrescrevem as imagens da primeira execução. Adicione um sufixo como -retry a outDir antes da segunda passagem, para que as duas execuções fiquem na mesma pasta.
Tempo do pior caso
Um site lento pode consumir todas as novas tentativas. O timeout e a quantidade de tentativas definem o pior caso para uma URL, e o limite distribui esse custo entre os workers. A figura abaixo usa os valores do script.
O pior caso é improvável em um lote saudável, mas define um teto. Agende o job de modo que uma execução com muitos timeouts termine antes do início da execução seguinte.
Repita somente erros que podem mudar
Um timeout de navegação ou uma conexão interrompida pode dar certo em uma segunda tentativa. Uma página que carrega por completo, mas não tem o seletor esperado, normalmente falha da mesma forma de novo. O mesmo vale para uma página que retorna status 404. Repita o primeiro grupo e registre o segundo grupo uma vez. A função shootWithRetry acima repete qualquer erro, então uma falha fixa custa três timeouts completos antes de aparecer no relatório.
Uma estrutura de pastas prática
site-shots/
run.mjs
urls.txt
shots/
2026-04-10/
001.jpg
002.jpg
report.json
Uma pasta por data de execução e um relatório por pasta. Essa estrutura basta para comparar execuções depois, sem usar banco de dados.
Erros comuns
- Usar
networkidle0em todos os sites. Páginas com analytics ou widgets de chat nunca estabilizam. - Deixar páginas abertas. A memória cresce até o processo ficar lento ou travar.
- Executar páginas demais ao mesmo tempo. Um limite de 20 em um notebook costuma causar mais timeouts, não execuções mais rápidas.
- Capturar sobreposições de consentimento. Clique no botão de recusa pelo seletor ou oculte o banner com
page.addStyleTag. - Ignorar os termos do site. A captura automatizada envia requisições reais. Verifique os termos de cada site e mantenha a taxa de requisições baixa.
Solução de problemas por sintoma
| Sintoma | Causa provável | Correção |
|---|---|---|
| Área em branco em uma imagem de página inteira | Conteúdo carregado sob demanda nunca entrou na tela | Execute a passagem de rolagem antes da captura |
| Timeout em goto | A página nunca alcança o evento de espera | Espere por um seletor ou troque para domcontentloaded |
| A mesma imagem para todas as URLs | A captura rodou antes de a navegação terminar | Use await em goto em cada página e abra uma página nova por URL |
| Imagem com o dobro do tamanho esperado | deviceScaleFactor é 2 | Remova o fator ou defina-o como 1 |
| A memória cresce em uma execução longa | As páginas permanecem abertas depois da captura | Feche cada página em um bloco finally e limite a concorrência |
| Banner de consentimento na imagem | O banner não foi tratado | Clique no botão de recusa pelo seletor ou oculte o banner |
Alternativas a um script de captura com Puppeteer
- Playwright tem uma API semelhante e também controla o WebKit. Veja Como automatizar capturas de tela de sites com Playwright.
- APIs de captura de tela hospedadas executam o navegador por você e devolvem as imagens por HTTP.
- Extensões de navegador, como a extensão Temposhot, salvam um PNG de página inteira da aba ativa.
- Temposhot Website Capture executa uma lista de URLs colada em um Mac sem código, com os tamanhos Desktop 1920×1080, Tablet 768×1024 ou Mobile 390×844 e atraso de 0 a 60 s por linha.
Quando o script não compensa
Um script custa mais com o tempo do que sua primeira versão sugere. Cada mudança no site, atualização do Node.js ou do Chrome pode exigir um ajuste. Uma tarefa mensal sobre uma lista que muda com frequência custa menos no Website Capture, onde a lista de URLs e as configurações de cada linha ficam na mesma janela. Mantenha o script quando as imagens alimentarem uma etapa de build, quando um pipeline de CI precisar executá-lo ou quando as etapas forem além de uma visita e uma captura.
O mesmo lote no Temposhot Website Capture
O Temposhot Website Capture cobre a tarefa em lote deste post sem script. As configurações abaixo correspondem ao executor da captura de página inteira em 1440 por 900, com as opções de espera e bloqueio que o Temposhot oferece por linha.
O tamanho do dispositivo difere do script de propósito: o Desktop no Temposhot tem 1920 por 1080, enquanto o script usa 1440 por 900. Defina o mesmo viewport nos dois se as saídas precisarem coincidir. Uma execução no Temposhot termina com ZIP, Images ou PDF na barra de resultados, que exporta os quadros selecionados.
Cada linha mostra o estado durante a execução, como Capturing ou Capture failed. Uma linha com falha pode ser repetida isoladamente, sem reiniciar o restante do lote.
FAQ
O Puppeteer funciona com o Firefox?
Sim. As versões recentes podem iniciar o Firefox, além do Chrome. O Chrome continua sendo a opção mais usada.
O Puppeteer consegue salvar capturas em WebP?
Sim. Defina type: 'webp' nas opções da captura. O valor de quality se aplica ao WebP da mesma forma que ao JPEG.
Por que page.goto dá timeout em alguns sites?
A página não alcançou o evento de espera escolhido em 30 segundos. Troque para networkidle2 ou load, aumente o timeout ou espere por um seletor.
Por que parte da minha imagem de página inteira está em branco?
O conteúdo carrega quando entra na tela pela rolagem. Execute a passagem de rolagem da seção de lazy loading antes da captura e espere a última linha aparecer.
Quantas URLs uma execução consegue processar?
O limite vem da memória e do tempo, não do código. Meça um lote de 20 URLs na concorrência que você planeja usar, acompanhe a memória e aumente a partir daí. O Temposhot Website Capture permite que Concurrent browsers vá de 1 a 10, com 4 como padrão.
A saída das capturas deve ir para o Git?
Não. As imagens e o relatório são saídas de build que crescem a cada execução. Mantenha urls.txt e o script no controle de versão e guarde a pasta de saída onde o tamanho dela couber.
Referências
Precisa de automação de capturas de tela de sites sem código?
O Temposhot Website Capture executa o mesmo tipo de lote de URLs localmente em um Mac, com configurações por linha e sem script para manter. Veja automação de capturas de tela de sites ou o guia de opções de captura.
puppeteerwebsite-captureautomationnodejs
Termos usados aqui: full page screenshot, headless browser, playwright, puppeteer, screenshot api, screenshot automation, viewport screenshot
Transforme capturas de tela repetitivas em trabalho pronto
O Temposhot faz capturas por temporizador ou a cada clique e exporta o resultado em PDF, imagens, ZIP ou MP4.
Posts relacionados
- Como capturar screenshots de sites em lote no MacCapture screenshots de sites em lote no Mac com o Temposhot. Cole uma lista de URLs, escolha os predefinidos de desktop, tablet e mobile, execute a captura em paralelo e exporte para auditorias ou relatórios.
- Como automatizar capturas de tela de sites com PlaywrightUse o Playwright para automatizar capturas de tela de sites com JavaScript. Capture uma página, páginas inteiras, visualizações mobile e lotes de URLs com novas tentativas.
- Como automatizar capturas de tela de sites sem códigoEscolha uma forma sem código de automatizar capturas de tela de sites: captura por lista de URLs, plataforma de automação com API de capturas, ferramentas de monitoramento ou extensões do navegador.
- Melhores alternativas ao Xnapper para Mac em 2026Compare alternativas ao Xnapper para Mac em captura repetida, captura e gravação com rolagem, fundos gratuitos, guias multiplataforma e o app nativo do sistema.