Pular para o conteúdo

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.

Checklist de seis sinais de que o Puppeteer se encaixa: já existe um projeto Node.js, as capturas rodam em agenda ou no CI, os arquivos de saída alimentam outro programa, a equipe revisa mudanças de script como qualquer código, um servidor executa a tarefa e a tarefa precisa de etapas personalizadas no navegador.

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.

Árvore de arquivos do projeto site-shots: arquivos do pacote, os scripts one e run, um arquivo de texto urls e uma pasta shots com uma subpasta datada por execução.

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.

Configurações do script de uma página: viewport de 1440 por 900 pixels CSS, fator de escala do dispositivo 2, espera até a rede ficar ociosa com zero conexões, saída em PNG e tamanho de saída de 2880 por 1800 pixels.

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.

Quatro estatísticas de uma captura de página inteira de um artigo longo com 1440 pixels de largura: 12000 pixels de altura, 17,3 milhões de pixels, cerca de 2,1 vezes a área de um quadro 4K e configuração de qualidade JPEG 80.

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.

Fluxo de cinco caixas: ler urls.txt, criar a pasta de saída com data, iniciar um navegador, capturar cada URL em sua própria página e gravar report.json com todos os resultados.

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.

Comparação dos limites de concorrência 1 e 10 para 200 URLs com cerca de 6 segundos por página: uma página por vez leva cerca de 20 minutos, e dez páginas por vez levam cerca de 2 minutos mais o custo de memória de dez renderizadores.

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.

Comparação de duas configurações móveis: emulate com um descritor de dispositivo altera o viewport, a razão de pixels, o suporte a toque e o user agent, enquanto setViewport com isMobile altera apenas o viewport e o comportamento de toque.

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.

Linha do tempo da passagem de rolagem em uma página de 8000 pixels: rolagem para 0 em 0,0 segundo, 900 pixels em 0,2 segundo, 7200 pixels em 1,6 segundo, e de volta ao topo em 1,8 segundo.

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çãoRetorna quando
loadO evento load é disparado. Este é o padrão.
domcontentloadedO HTML é analisado.
networkidle0Não há conexões de rede por pelo menos 500 ms.
networkidle2Nã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.

Checklist de sinais de prontidão para uma página de dashboard: o contêiner do gráfico principal está presente, o spinner de carregamento foi removido, a última linha de dados está visível, as fontes web terminaram de carregar e o banner de consentimento está fechado.

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.

Quatro estatísticas do pior caso: 3 tentativas por URL, 45 segundos por tentativa, 135 segundos por URL quando todas as tentativas expiram e cerca de 56 minutos para 100 URLs com 4 workers.

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

  1. Usar networkidle0 em todos os sites. Páginas com analytics ou widgets de chat nunca estabilizam.
  2. Deixar páginas abertas. A memória cresce até o processo ficar lento ou travar.
  3. 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.
  4. Capturar sobreposições de consentimento. Clique no botão de recusa pelo seletor ou oculte o banner com page.addStyleTag.
  5. 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

SintomaCausa provávelCorreção
Área em branco em uma imagem de página inteiraConteúdo carregado sob demanda nunca entrou na telaExecute a passagem de rolagem antes da captura
Timeout em gotoA página nunca alcança o evento de esperaEspere por um seletor ou troque para domcontentloaded
A mesma imagem para todas as URLsA captura rodou antes de a navegação terminarUse await em goto em cada página e abra uma página nova por URL
Imagem com o dobro do tamanho esperadodeviceScaleFactor é 2Remova o fator ou defina-o como 1
A memória cresce em uma execução longaAs páginas permanecem abertas depois da capturaFeche cada página em um bloco finally e limite a concorrência
Banner de consentimento na imagemO banner não foi tratadoClique no botão de recusa pelo seletor ou oculte o banner
Lista de fazer e não fazer para um script de captura de tela: feche cada página, limite a concorrência, espere por um sinal de prontidão e mantenha um relatório; não use networkidle0 em todos os casos e não reutilize uma pasta entre execuções.

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.

Configurações de um lote do Temposhot Website Capture: dispositivo Desktop, área Full page, espera Network idle, atraso de 2 segundos, banners de cookies recusados, anúncios e rastreadores bloqueados, JPEG com qualidade 80 e 4 navegadores simultâneos.

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.

Cinco etapas para executar o lote de URLs no Temposhot Website Capture: colar a lista com Bulk insert, definir Concurrent browsers em Run Settings, iniciar a captura, repetir as linhas com falha e exportar os resultados em ZIP.

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.

Comparação dos estados de linha: Capture pending aguarda uma vaga de navegador, Capturing carrega a página e Capture complete indica que a captura foi salva; Capture failed indica que nenhuma captura foi gerada.

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