Como lidar com imagens de mula sem cabeça em fluxos de automação
Vou ser direto. No dia a dia com scraping e geração de relatórios visuais, eu tropecei várias vezes num problema que parecia bobeira até virar dor de cabeça real: imagens que saíam corrompidas ou incompletas quando o processo rodava sem interface gráfica visível. Eu chamava isso de imagem mula sem cabeça porque a saída vinha com o corpo inteiro, mas faltava algo essencial no meio do caminho. O arquivo existia, tinha tamanho, extensão .png ou .jpg, mas ao abrir davava branco, pixelado ou com o conteúdo truncado no cabeçalho.
O que é imagem mula sem cabeça na prática
Não é um termo oficial da documentação de nenhuma ferramenta. É gíria de quem briga com headless Chrome, Puppeteer, Playwright ou Selenium rodando em servidor Linux sem monitor. A "mula" é a imagem renderizada. O "sem cabeça" é o cabeçalho PNG, os metadados EXIF, a paleta de cores ou simplesmente o canvas que não foi devidamente inicializado antes do save. Eu já passei uma tarde inteira debugando um script que funcionava perfeito na minha máquina local e produzia arquivos defeituosos no container Docker. A solução? Não foi mágica. Foi adicionar um delay de 300ms após o load do DOM e forçar o redraw do canvas antes de chamar o toDataURL(). Simples assim, mas só descobri porque o erro era intermitente e aparecia apenas sob carga alta de CPU no servidor.
Por que isso acontece e onde você vai se dar mal
O navegador headless não tem o mesmo ciclo de pintura que uma sessão com UI. Quando você pede para capturar uma tela ou renderizar um canvas, o motor de rendering pode ainda estar processando camadas de layout. O arquivo é gerado antes da hora. O resultado é exatamente o que a galera chama de imagem mula sem cabeça: formato válido, mas conteúdo visual incompleto. Outro cenário clássico é exportar SVG para PNG usando bibliotecas como svg2img ou sharp sem definir viewBox ou dimensões explícitas. O SVG nasceu sem tamanho definido, a conversão não sabe onde cortar, e você recebe um PNG de 1x1 pixel ou um arquivo cheio de artefatos. Isso não tem relação direta com headless, mas Entra na mesma categoria de "imagem que parece que nasceu sem cabeça".
Tem ainda o problema dos fonts. Se a renderização acontece antes do fonte carregar, o canvas é pintado com fallback system font. A imagem salva, mas o texto aparece completamente diferente do esperado. Você acha que o processo falhou, quando na verdade foi só timing.
Workarounds que realmente funcionam
A primeira coisa que eu faço é adicionar waitForFunction() ou uma promise que resolve quando document.fonts.ready estiver resolvida. Em Playwright: await page.waitForFunction(() => document.fonts.ready)
👉 Clique no botão abaixo para saber mais sobre o assunto!
Isso custa 50 a 200ms na maioria das páginas, mas elimina 90% dos casos de texto renderizado errado. Para canvas, use requestAnimationFrame() duas vezes antes de capturar. O browser precisa de pelo menos dois frames de pintura para estabilizar o conteúdo: await page.evaluate(() => { requestAnimationFrame(() => requestAnimationFrame(() => {})) })
Se você está convertendo SVG, sempre defina width e height explícitos no elemento
Quando essa abordagem não funciona
Se o problema estiver no servidor de origem da imagem, nenhum delay do lado do cliente vai resolver. Já vi caso de uma API de geração de QR code que retornava imagens truncadas porque o endpoint timeoutava antes de completar a escrita do buffer. Nesse cenário, a correção é no backend, não no frontend. Também não adianta aplicar esses truques se você estiver usando uma biblioteca obsoleta de renderização. A versão do Puppeteer importa. A versão do Chrome headless importa. A biblioteca de conversão de imagem importa. Eu já vi pessoas gastarem horas debugando um problema que era incompatibilidade entre puppeteer 13.x e chrome headless 115. Atualizar tudo para versões alinhadas resolveu em 10 minutos.
Se o seu caso é realmente pesado, com centenas de imagens por hora, considere usar uma fila com retry exponencial em vez de depender só de delays. Um job que falha pode ser retryado automaticamente com backoff de 1s, 2s, 4s. Em média, isso captura 99% dos casos de imagem mula sem cabeça sem precisar de sincronia manual complexa.
Custo-benefício dos truques
Adicionar o waitForFunction de fonts custa em média 120ms por página. Dois requestAnimationFrame() custam cerca de 33ms cada, dependendo do refresh rate do monitor virtual. Desativar a GPU com --disable-gpu pode aumentar o tempo de renderização em 15 a 30% em cenários com muitos elementos absolutos posicionados, mas reduz drasticamente artefatos visuais em containers sem driver. No geral, o investimento de tempo para implementar esses ajustes é de 20 a 40 minutos na primeira vez. Depois vira padrão no seu projeto e você não pensa mais no assunto. O retorno é evitar horas de debug noturno quando o relatório visual sai errado em produção.