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: 2024-11-27

Melhorando as métricas de LCP com web workers e carregamento de imagem em 2 etapas

Na maioria das vezes, um elemento de imagem grande na viewport visível se tornará o elemento Largest Contentful Paint. Mesmo depois de aplicar todas as melhores práticas do Lighthouse, como redimensionamento de imagens, compressão de imagens, conversão para WebP e preload do elemento LCP, seu Largest Contentful Paint ainda pode não passar nos Core Web Vitals.

A única maneira de corrigir isso é usando táticas mais avançadas como carregamento em 2 etapas e threading da sua página com web workers para liberar recursos na thread principal.

Neste artigo, mostrarei como melhorar ainda mais o Largest Contentful Paint.

Por que devo fazer preload da imagem Largest Contentful Paint

Um pouco de contexto

Eu sou um cara de pagespeed e meu site é minha vitrine. Na minha homepage, orgulhosamente afirmo que meu site é o site mais rápido do mundo. É por isso que preciso que minha página carregue o mais rápido possível e extraia cada gota de pagespeed do meu site.

As técnicas que mostrarei hoje podem não ser viáveis para o seu site médio (WordPress) sem o suporte de uma equipe de desenvolvimento dedicada e talentosa. Se você não conseguir duplicar essa técnica em seu próprio site, ainda assim encorajo você a ler o artigo e aprender como eu penso sobre pagespeed e quais são minhas considerações.

O problema: imagens grandes na viewport visível

Uma imagem grande na viewport visível frequentemente se tornará o elemento Largest Contentful Paint. É comum que essa imagem LCP não passe nos Core Web Vitals. Eu vejo resultados como este diariamente.

LCP ruim com imagem grande

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

  1. Faça preload do elemento LCP. Fazer preload da imagem LCP garantirá que esta imagem esteja disponível para o navegador o mais cedo possível.
  2. Use imagens responsivas. Certifique-se de que não está servindo imagens de tamanho desktop para dispositivos móveis.
  3. Comprima suas imagens. A compressão de imagens pode reduzir drasticamente o tamanho da imagem
  4. Use formatos de imagem de nova geração. Formatos de imagem de nova geração como WebP superam formatos mais antigos como JPEG e PNG em quase todos os casos.
  5. Minimize o caminho crítico de renderização. Elimine todos os recursos que bloqueiam a renderização, como JavaScripts e folhas de estilo, que possam atrasar o LCP.

Infelizmente, apesar de todas essas otimizações, em alguns casos, as métricas de LCP ainda podem não passar na auditoria dos Core Web Vitals. Por quê? O tamanho da imagem sozinho é suficiente para atrasar o LCP.

A solução: carregamento em 2 etapas e web workers

A solução que implementei (depois de otimizar todos os outros problemas do meu site) é o carregamento de imagem em 2 etapas.

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

Uma implementação muito básica pode parecer algo assim: Primeiro, adicione um event listener de load a uma imagem. Quando a imagem carrega, esse mesmo event listener se desanexa e o src da imagem é trocado pela imagem final de alta qualidade.

<img 
     width="100" 
     height="100" 
     alt="algum texto alternativo" 
     src="lq.webp" 
     onload="this.onload=null;this.src='hq.webp'"
>

Etapa 1: webp de baixa qualidade 3-5kb

Etapa 2: webp de alta qualidade 20-40kb

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

É por isso que escolhi descarregar 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 é feita através de um sistema de mensagens. A vantagem óbvia é que não estamos usando a thread principal da página em si, estamos liberando recursos lá. 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, coleto todas as imagens da página. Se uma imagem já foi carregada, farei a troca imediatamente. Se não foi carregada (porque a imagem pode fazer lazy load), anexarei um event listener que troca a imagem após o lazy load.

O resultado: espetacular

good lcp visible image

O código para carregamento de LCP em 2 etapas através de um web worker

Aqui está o código que uso para acelerar meu LCP através de carregamento em 2 etapas e 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 uma única função. Ele escuta mensagens. Uma mensagem conterá uma URL de imagem e um id único de imagem. Primeiro, ele transformará a URL da imagem para a 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 o id único.
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 roda como um script normal na página ativa. O script primeiro carrega o worker. Em seguida, percorre todas as imagens da página. Isso acontece 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, chamará o processo de troca imediatamente. Se ainda não estiver carregada, anexará um listener ao evento de load da imagem que inicia o processo de troca assim que a imagem for carregada.
Quando uma imagem é carregada, um id único é gerado para ela. Isso me permite encontrar facilmente a imagem na página novamente (lembre-se, o worker não tem acesso ao DOM, então não posso enviar o nó DOM da imagem).
A URL da imagem e o id único são então enviados ao worker.
Quando o worker busca a imagem, ela é enviada de volta ao script como um blob. O script eventualmente troca a URL antiga da imagem pela URL do blob que foi criado 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 }
                )
        })
})

Pontuação de Core Web Vitals com imagem LCP pré-carregada

Make decisions with Data.

You cannot optimize what you do not measure. Install the CoreDash pixel and capture 100% of user experiences.

Create Free Account >>

  • 100% Capture
  • Data Driven
  • Easy Install
Como reduzi meu LCP em 70%Core Web Vitals Como reduzi meu LCP em 70%