100/100 Core Web Vitals, estudo de caso

100/100 Core Web Vitals, estudo de caso

Última verificação: 29 de agosto de 2026
12 min de leitura
Guia
Core Web Vitals

Uma pontuação de 100 no PageSpeed começa no servidor, não no navegador. É por aí que entramos neste caso: uma loja WooCommerce de decoração de casa que abria em 4,8s de LCP, com INP de 450ms e CLS de 0,25, e cujo TTFB sozinho já comia a maior parte do orçamento de 2,5s que a métrica de LCP permite.

A conta é trivial e é ela que decide tudo o resto. Se o servidor demora 600ms a devolver o primeiro byte, sobram menos de 1,9s para descarregar o HTML, descobrir a imagem principal, pedi-la, descodificá-la e pintá-la. Nenhum truque de CSS crítico compensa esse atraso inicial. Por isso a primeira semana do trabalho não teve uma única linha de front-end.

#Medir o TTFB antes de tocar em qualquer coisa

O primeiro comando de todo o projeto foi este, repetido a partir de três máquinas em países diferentes:

curl -o /dev/null -s -w "dns:%{time_namelookup} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" https://exemplo.pt/

Vale a pena separar as parcelas. Um TTFB alto pode ser resolução de DNS lenta, handshake TLS a cada pedido por falta de keep-alive, ou geração de página em PHP. São três problemas sem nada em comum e cada um tem uma correção diferente. Medir o agregado e adivinhar a causa é como este tipo de trabalho costuma descarrilar.

Nesta loja, o time_appconnect era saudável e o time_starttransfer disparava. O problema estava dentro do PHP. O Query Monitor, com SAVEQUERIES ativo num ambiente de testes, mostrou o padrão clássico do WooCommerce maduro: várias centenas de consultas por pedido, uma boa parte delas repetidas dentro do mesmo carregamento, e uma tabela wp_options com autoload inflado por anos de plugins instalados e desinstalados sem limpeza.

SELECT SUM(LENGTH(option_value)) AS bytes, COUNT(*) AS n
FROM wp_options WHERE autoload = 'yes';

Quando esse número passa de alguns megabytes, cada pedido, incluindo os de admin-ajax.php, carrega tudo isso para memória antes de fazer seja o que for. Limpar autoload não é glamoroso e é das intervenções com melhor relação entre esforço e milissegundos poupados.

#Cache de objetos: onde o Redis ajuda e onde não

Instalámos Redis no mesmo host, com o drop-in object-cache.php do plugin Redis Object Cache, e validámos com wp redis status antes de acreditar em qualquer coisa. O detalhe que engana muita gente: o cache de objetos do WordPress é, por omissão, não persistente. Sem drop-in, os dados guardados com wp_cache_set() morrem no fim do pedido. Com Redis, sobrevivem entre pedidos, e é aí que está o ganho.

O mecanismo é simples. Menus, taxonomias, termos, opções, meta de produtos e resultados de WP_Query passam a ser lidos da memória em vez do MySQL. O tempo de geração da página deixa de crescer linearmente com o número de consultas.

O que o Redis não resolve, e convém dizê-lo:

  • Código lento continua lento. Um loop que faz get_post_meta() dentro de um foreach sobre 300 produtos continua a fazer 300 chamadas, mesmo que cada uma seja rápida. O cache reduz o custo unitário, não o número de chamadas.
  • Invalidação em massa. Uma importação de stock que toca em milhares de produtos limpa grupos inteiros de cache. Nos minutos seguintes, o site fica mais lento do que estava, porque a cache está fria e o tráfego ainda chega. Agendar importações fora dos picos é parte da otimização, não um detalhe operacional.
  • Memória finita. Sem maxmemory e uma política de despejo definida, o Redis enche e começa a recusar escritas. Com allkeys-lru definido, despeja as chaves menos usadas e continua a servir. Definir isto antes do primeiro pico é mais barato do que diagnosticá-lo durante.

Ao mesmo tempo, desligámos o wp-cron.php disparado por visita (define('DISABLE_WP_CRON', true);) e passámos a chamá-lo por cron de sistema a cada minuto. Numa loja com tráfego, o cron por visita significa que um visitante aleatório paga, no TTFB dele, o custo de gerar relatórios de vendas ou de enviar emails pendentes.

#Edge cache de HTML, e o momento em que a personalização parte tudo

Redis reduz o tempo de geração. O edge cache elimina-o para a maioria dos pedidos, porque o HTML já está num ponto de presença perto do visitante e o pedido nunca chega ao origem.

Para páginas de catálogo, categorias e conteúdo editorial, isto funciona bem. A regra que usámos guarda o HTML no edge com s-maxage alto e permite servir uma cópia ligeiramente antiga enquanto revalida em segundo plano:

Cache-Control: public, max-age=0, s-maxage=86400, stale-while-revalidate=600

O max-age=0 mantém o navegador honesto (pede sempre), o s-maxage deixa o edge servir de memória, e o stale-while-revalidate evita que a primeira visita depois de uma expiração pague o custo total de geração.

E é aqui que uma loja é diferente de um blogue. O HTML do WooCommerce não é o mesmo para toda a gente. O mini-carrinho, o “Olá, Ana”, os preços com IVA por país, os produtos vistos recentemente: tudo isso torna uma cópia partilhada ou errada ou perigosa. Servir a um visitante o HTML em cache de outro, com o carrinho dele lá dentro, é o pior resultado possível deste trabalho, pior do que um site lento.

As regras que impedem isso, e que devem ser escritas antes de ligar o cache e não depois:

  1. Bypass por cookie. Qualquer pedido com wordpress_logged_in_*, woocommerce_items_in_cart, woocommerce_cart_hash ou wp_woocommerce_session_* vai direto ao origem e não é guardado.
  2. Bypass por caminho. /carrinho/, /finalizar-compra/, /minha-conta/ e tudo sob wp-admin nunca são guardados, mesmo sem cookie.
  3. Bypass por query string. URLs com add-to-cart=, wc-ajax= ou parâmetros de pagamento não são cacheáveis.

A parte dinâmica que sobra tem de voltar por outro caminho. O WooCommerce já traz um: os fragmentos AJAX (wc-ajax=get_refreshed_fragments), que pedem ao servidor apenas o pedaço de HTML do mini-carrinho depois de a página estar pintada. Funciona, mas tem custo. Esse pedido não é cacheável por definição, acontece em todas as páginas, e em lojas com muitos plugins arrasta meio WooCommerce na inicialização. Nesta loja, limitámos os fragmentos às páginas onde o mini-carrinho existe de facto e reconstruímos o contador a partir de estado guardado no cliente nas restantes. O compromisso é assumido: em caso de divergência, o contador pode ficar desatualizado alguns segundos, e a página do carrinho, que nunca é servida de cache, é a fonte de verdade.

A alternativa mais limpa é montar o recorte no edge, com um Worker a injetar o fragmento personalizado no HTML em cache antes de o devolver. É a ideia antiga do ESI, com ferramentas melhores. Ganha-se um TTFB de página estática com conteúdo por utilizador. Paga-se em complexidade de depuração: passa a existir lógica em três sítios (PHP, edge, navegador) e um bug de carrinho obriga a percorrer os três.

Uma nota sobre invalidação, porque é o erro mais comum depois de tudo isto estar a funcionar. Se uma publicação de produto não limpa a cópia no edge, o site fica rápido e errado, e ninguém repara durante dias porque a métrica que se anda a observar continua verde. Ligar o purge à ação real do WordPress, e não a um intervalo de tempo, é o que faz a diferença entre um cache que ajuda e um que cria incidentes.

#Afinação de PHP, OPcache e base de dados

Entre o cache de objetos e o edge há uma camada que raramente aparece em artigos de performance porque não é visível no PageSpeed: a configuração do próprio runtime.

O OPcache guarda o bytecode compilado dos ficheiros PHP em memória partilhada. Se estiver desligado, ou com memória insuficiente, cada pedido recompila milhares de ficheiros do WordPress, do WooCommerce e dos plugins. Os valores que verificámos primeiro foram opcache.memory_consumption e opcache.max_accelerated_files. O segundo é o que costuma estar errado: uma loja madura passa facilmente os dez mil ficheiros PHP e o valor por omissão fica abaixo disso, pelo que parte do código é recompilado a cada pedido enquanto o painel mostra o OPcache como ativo. Em servidores onde o deploy é atómico, opcache.validate_timestamps=0 remove ainda a verificação de data em cada ficheiro, com a condição de que o deploy passe a limpar o OPcache explicitamente. Sem essa condição cumprida, o site serve código antigo depois de uma atualização, e esse sintoma leva horas a diagnosticar porque tudo o resto parece correto.

Do lado do PHP-FPM, o número de processos filho define quantos pedidos não cacheados a loja aguenta em simultâneo. Demasiado baixo e os pedidos ficam em fila com o CPU a dormir. Demasiado alto e o servidor entra em swap, altura em que o TTFB deixa de ser medido em milissegundos. O cálculo de partida é memória disponível a dividir pelo consumo médio de um processo, medido em produção com ps durante um pico, nunca estimado.

Na base de dados, as duas alavancas foram o tamanho do buffer pool do InnoDB, que deve comportar o conjunto de dados quente, e os índices em wp_postmeta, onde as consultas de atributos e variações do WooCommerce se concentram. Query Monitor identifica as consultas lentas, e EXPLAIN diz se estão a fazer varrimento de tabela. Nenhuma camada de cache compensa uma consulta que varre um milhão de linhas sempre que a cache expira.

#Camada do navegador: LCP, CLS e INP sobre uma base já rápida

Com o TTFB medido em dezenas de milissegundos em vez de centenas, o resto do trabalho passou a ter efeito visível.

LCP. O elemento maior era um slider que carregava alguns megabytes de JavaScript antes de mostrar a primeira imagem. Substituímo-lo por um bloco estático em CSS Grid, com a imagem principal em AVIF, servida com fetchpriority="high" e sem loading="lazy". Este par de atributos é a correção mais mal aplicada em WordPress: colocar loading="lazy" na imagem de topo atrasa deliberadamente aquilo que a métrica está a cronometrar. A conversão de PNG para AVIF reduziu a imagem de cabeçalho de 800KB para 45KB, com fallback WebP para os agentes que ainda não suportam AVIF.

CLS. Duas causas, uma correção cada. As fontes deslocavam o texto quando substituíam a fonte de sistema, e ficou resolvido com <link rel="preload"> na fonte primária mais font-display: optional, que aceita ficar com a fonte de sistema quando a personalizada não chega a tempo. É uma troca explícita: perde-se consistência tipográfica numa minoria de visitas, ganha-se zero deslocamento em todas. As imagens em lazy loading empurravam o conteúdo por falta de espaço reservado, e a correção foi aspect-ratio nos contentores, que funciona em layouts responsivos onde width e height fixos não funcionam.

INP. O conflito estava na thread principal: widget de chat, Pixel, gestor de tags e ferramentas de mapa de calor a competir com o WooCommerce. Movemos os scripts de terceiros para um Web Worker com Partytown. Convém saber o que isso implica antes de o prometer a um cliente: o Partytown faz proxy dos pedidos por um endpoint no mesmo domínio, precisa de ficheiros de biblioteca servidos com os cabeçalhos certos, e nem todos os scripts sobrevivem à mudança. Os que tocam diretamente no DOM ou dependem de temporização síncrona partem-se, e a forma de descobrir quais é testar um a um, não confiar na lista de compatibilidade.

#Speculation rules, e os sítios onde não a deve ligar

A última camada é percetual. Com as Speculation Rules, o navegador pode ir buscar ou pré-renderizar a página seguinte antes do clique:

<script type="speculationrules">
{
  "prerender": [{
    "where": { "href_matches": "/produto/*" },
    "eagerness": "moderate"
  }]
}
</script>

moderate dispara com a passagem do rato ou o início do toque, o que é suficiente para eliminar a espera percebida sem pré-renderizar tudo o que aparece no ecrã. eager parece melhor em demonstração e desperdiça largura de banda e ciclos de CPU em dispositivos móveis, que é precisamente onde a pontuação estava pior.

Três armadilhas reais, todas verificáveis em minutos:

  • Uma pré-renderização executa JavaScript. Analytics, testes A/B e contadores de vista disparam numa página que o utilizador pode nunca abrir. Quem não trata disso passa a ter métricas de negócio infladas por causa de uma otimização de velocidade. A defesa está no próprio DOM: verificar document.prerendering e adiar o envio até ao evento prerenderingchange.
  • Nunca pré-renderizar ações com efeitos. Ligações com add-to-cart=, logout ou qualquer coisa sob /finalizar-compra/ ficam de fora por regra, não por bom senso do programador que escrever o próximo link.
  • Interação com o edge cache. Pré-renderizar páginas personalizadas pede ao origem exatamente aquilo que as regras de bypass acima protegem. As duas configurações têm de concordar sobre o que é público.

#O que não fizemos, e porquê

Ficou de fora a reescrita da montra em frontend desacoplado. Teria dado números de laboratório melhores e teria custado a integração de checkout, o SEO das páginas de categoria e meses de trabalho para resolver o que a camada de cache já resolvia. Ficou também de fora a substituição do tema, porque o problema não estava no tema, estava no que corria antes dele.

E fica um aviso sobre a própria pontuação. 100 no Lighthouse é uma medição de laboratório, feita numa máquina, com uma rede simulada, sem extensões e com a cache fria. Os dados que contam para a avaliação de experiência de página são de campo, do CrUX, recolhidos em dispositivos reais em 28 dias. Uma loja pode ter 100 em laboratório e ficar fora do limiar em campo, porque metade dos visitantes usa telemóveis antigos em rede móvel. Por isso instrumentámos a biblioteca web-vitals com a build de atribuição, que devolve não só o valor de INP mas também o seletor do elemento que o causou. Sem isso, otimizar INP é adivinhar.

#Conclusão

Velocidade não é dívida técnica, é arquitetura. A ordem importa: servidor, cache de objetos, cache no edge, regras de personalização, e só depois o navegador. Feita ao contrário, cada correção de front-end é medida sobre uma base que se move, e o resultado desfaz-se no primeiro pico de tráfego.

O seu site WooCommerce está lento? A WPPoland trabalha da infraestrutura para cima, e diz-lhe onde está o limite antes de começar.

Próximo passo

Transforme o artigo numa implementação real

Este bloco reforça a ligação interna e conduz o leitor para o passo seguinte mais útil dentro da arquitetura do site.

Quer implementar isto no seu site?

Se o problema está nos Core Web Vitals, no rendering lento ou no peso do WordPress, posso mapear e implementar a otimização.

Cluster relacionado

Explorar outros serviços WordPress e base de conhecimento

Reforce o seu negócio com suporte técnico profissional em áreas-chave do ecossistema WordPress.

FAQ do artigo

Perguntas frequentes

Respostas práticas para aplicar o tema na execução real.

SEO-readyGEO-readyAEO-ready3 Q&A
Usaram o WP Rocket?#
Sim, más como base. A pontuação de 100 exigiu código personalizado para geração de 'CSS Crítico' em páginas de produto dinâmicas e Regras de Especulação para prefetching.
Como corrigiram o INP no WooCommerce?#
O botão AJAX 'Adicionar ao Carrinho' era o gargalo. Refatorizámo-lo para usar um Web Worker (Partytown) para manter a thread principal livre.
Isto é possível em Alojamento Partilhado?#
Extremamente difícil. Este resultado dependeu de Redis para Object Caching é um CDN para Edge Caching. O TTFB (Time to First Byte) em alojamento partilhado é geralmente demasiado lento para uma pontuação de 100.

Precisa de FAQ adaptado ao setor e mercado? Criamos uma versão alinhada com os seus objetivos de negócio.

Fale connosco

Artigos Relacionados

Demasiados plugins WordPress

Um site de comparação de seguros chegou com mais de 30 plugins, uma base de dados de 705 MB e um LCP de 7.7s. O pior culpado era um contador de visualizações a escrever em wp_postmeta a cada carregamento. Um teardown real do padrão de excesso de plugins que os projetos rápidos e assistidos por IA continuam a produzir.