Como reduzi meu LCP em 70%

Aprenda métodos avançados para melhorar os Core Web Vitals

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-03-04

Melhorando as métricas do LCP com Web Workers e carregamento de imagem em 2 estágios

Na maioria das vezes, uma imagem grande no viewport visível se torna o elemento do Largest Contentful Paint. Mesmo após aplicar todas as práticas recomendadas do Lighthouse, como redimensionamento, compressão, conversão para WebP e preloading do elemento do LCP, seu Largest Contentful Paint ainda pode não passar no Core Web Vitals.

A única maneira de corrigir isso é usar táticas mais avançadas, como carregamento em 2 estágios e a execução da sua página em threads com Web Workers para liberar recursos no main thread.

Why should I preload the largest contentful paint image

Última revisão por Arjen Karel em março de 2026

Um pouco de contexto

Sou especialista em velocidade e meu site é minha vitrine. Na minha página inicial, afirmo com orgulho que meu site é o mais rápido do mundo. É por isso que preciso que minha página carregue o mais rápido possível e que eu extraia cada gota de velocidade do meu site.

As técnicas que mostrarei hoje podem não ser viáveis para um site comum (WordPress) sem o suporte de uma equipe de desenvolvimento dedicada e talentosa. Se você não conseguir replicar essa técnica no seu próprio site, ainda assim recomendo que leia o artigo e entenda como penso sobre velocidade e quais são as minhas considerações.

O problema: imagens grandes no viewport visível

Uma imagem grande no viewport visível frequentemente se torna o elemento do Largest Contentful Paint. É comum que essa imagem do LCP não passe no Core Web Vitals. Vejo resultados como esse diariamente.

bad LCP with large image

Existem várias maneiras de garantir que esse elemento apareça na tela rapidamente:

  1. Faça o preload do elemento do LCP. Fazer o preload da imagem do LCP garante que ela esteja disponível para o navegador o mais cedo possível. Combine isso com fetchpriority="high" para orientar o navegador a priorizar essa imagem em relação a outros recursos.
  2. Use imagens responsivas. Certifique-se de não servir imagens com tamanho de desktop para dispositivos móveis.
  3. Comprima suas imagens. A compressão de imagem pode reduzir drasticamente o tamanho da imagem.
  4. Use formatos de imagem de nova geração. Formatos de imagem de nova geração, como o WebP, superam formatos antigos como JPEG e PNG em quase todos os casos.
  5. Minimize o caminho crítico de renderização. Elimine todos os recursos de render blocking, como JavaScript e folhas de estilo, que podem atrasar o LCP.

Infelizmente, apesar de todas essas otimizações, as métricas do LCP ainda podem não passar na auditoria do Core Web Vitals em alguns casos. Por quê? O tamanho da imagem por si só é suficiente para atrasar a fase de duração do carregamento do recurso do LCP.

A solução: carregamento em 2 estágios e Web Workers

A solução que implementei (após otimizar todos os outros problemas no meu site) é o carregamento de imagem em 2 estágios.

A ideia é simples: na primeira renderização, mostre uma imagem de baixa qualidade com exatamente as mesmas dimensões da imagem final de alta qualidade. Imediatamente após essa imagem ser exibida, inicie o processo que troca a imagem de baixa qualidade por uma imagem de alta qualidade.

Uma implementação muito básica seria assim: primeiro, adicione um listener de evento de load a uma imagem. Quando a imagem carrega, esse mesmo listener de evento se desconecta e o src da imagem é trocado pela imagem final de alta qualidade.

<img
     width="100"
     height="100"
     alt="some alt text"
     src="lq.webp"
     onload="this.onload=null;this.src='hq.webp'"
>

Estágio 1: webp de baixa qualidade 3-5kb two stage image loading lq

Estágio 2: webp de alta qualidade 20-40kb two stage image loading hq

Isso pode parecer simples (e é), mas trocar um grande número de imagens logo no início do processo de renderização causará muita atividade no main thread e afetará outras métricas do Core Web Vitals.

É por isso que decidi transferir parte do trabalho para um Web Worker. Um Web Worker roda em uma nova thread e não tem acesso real à página atual. A comunicação entre o Web Worker e a página ocorre por meio de um sistema de mensagens. A vantagem óbvia é que não estamos usando o main thread da própria página; estamos liberando recursos ali. A desvantagem é que usar um Web Worker pode ser um pouco trabalhoso.

O processo em si não é tão difícil. Assim que o evento DOMContentLoaded é disparado, eu coleto todas as imagens da página. Se uma imagem já estiver carregada, eu a troco imediatamente. Se não estiver carregada (porque a imagem pode ter lazy loading), eu anexo um listener de evento que troca a imagem após o lazy loading.

Uma ressalva importante: o navegador trata cada troca de imagem como um novo candidato ao LCP. Se a troca pela sua imagem de alta qualidade ocorrer após 2,5 segundos, o LCP será medido no momento da troca, não no tempo do placeholder. Por isso é importante que o Web Worker busque e troque a imagem o mais rápido possível.

O resultado: espetacular.

good lcp visible image

O código para o carregamento do LCP em 2 estágios por meio de um Web Worker

Aqui está o código que uso para acelerar meu LCP por meio do carregamento em 2 estágios e de um Web Worker. O código na página principal chama um Web Worker que buscará as imagens. O Web Worker passa o resultado como um blob para a página principal. Ao receber o blob, a imagem é trocada.

Worker.js

O worker tem apenas uma função. Ele escuta mensagens. Uma mensagem conterá a URL de uma imagem e uma ID única. Primeiro, ele transformará a URL da imagem na versão de alta qualidade. No meu caso, alterando /lq para /resize na URL da imagem. O worker então buscará a imagem de alta qualidade, criará um blob e retornará o blob da imagem junto com a ID única.
self.addEventListener('message', async event => {
    const newimageURL = event.data.src.replace("/lq-","/resize-");

    const response = await fetch(newimageURL)
    const blob = await response.blob()

    // Send the image data to the UI thread!
    self.postMessage({
        uid: event.data.uid,
        blob: blob,
    })
})

Script.js

O script.js rodará como um script normal na página da web ativa. O script primeiro carrega o worker. Em seguida, ele passará por todas as imagens de uma página. Isso acontece logo no início do processo de renderização. Uma imagem pode já estar carregada ou não. Se uma imagem de baixa qualidade já estiver carregada, ele chamará o processo de troca imediatamente. Se ainda não estiver carregada, ele anexará um listener ao evento de load da imagem que inicia o processo de troca assim que ela for carregada.

Quando uma imagem carrega, uma ID única é gerada para ela. Isso me permite encontrar a imagem novamente na página com facilidade (lembre-se, o worker não tem acesso ao DOM, então não posso enviar o DOM Node da imagem). A URL da imagem e a ID única são então enviadas ao worker. Quando o worker busca a imagem, ela é enviada de volta ao script como um blob. Por fim, o script troca a antiga URL da imagem pela URL do blob criada pelo Web Worker.

var myWorker = new Worker('/path-to/worker.js');

// send a message to worker
const sendMessage = (img) => {

        // uid makes it easier to find the image
        var uid = create_UID();

        // set data-uid on image element
        img.dataset.uid = uid;

        // send message to worker
        myWorker.postMessage({ src: img.src, uid: uid });
};

// generate the uid
const create_UID = () => {
    var dt = new Date().getTime();
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = (new Date().getTime() + Math.random() * 16) % 16 | 0;
        dt = Math.floor(dt / 16);
        return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });
    return uid;
}

// when we get a result from the worker
myWorker.addEventListener('message', event => {
    // Grab the message data from the event
    const imageData = event.data

    // Get the original element for this image
    const imageElement = document.querySelectorAll("img[data-uid='" + imageData.uid + "']");

    // We can use the `Blob` as an image source! We just need to convert it
    // to an object URL first
    const objectURL = URL.createObjectURL(imageData.blob)

    // Once the image is loaded, we'll want to do some extra cleanup
    imageElement.onload = () => {
        URL.revokeObjectURL(objectURL)
    }
    imageElement[0].setAttribute('src', objectURL)
})

// get all images
document.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll('img[loading="lazy"]').forEach(
        img => {

            // image is already visible?
            img.complete ?

                // swap immediately
                sendMessage(img) :

                // swap on load
                img.addEventListener(
                    "load", i => { sendMessage(img) }, { once: true }
                )
        })
})

Para verificar a melhoria do LCP em campo, use o Real User Monitoring para rastrear como seus visitantes reais experimentam a página. Ferramentas de laboratório como o Lighthouse mostrarão a melhoria, mas o field data de usuários reais em conexões variadas é o que conta para passar no Core Web Vitals.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Descobre o que é mesmo lento.

Mapeio o critical rendering path com dados RUM. Recebes uma lista de fixes por prioridade, não um relatório do Lighthouse.

Quero a auditoria
Como reduzi meu LCP em 70% Core Web Vitals Como reduzi meu LCP em 70%