PageSpeed mobile: como levei um site HTML estático de 64 para 97
Não sou programador. Sou corretor de planos de saúde e sócio de uma corretora, e cuido pessoalmente dos nossos sites. Nesta semana passei alguns dias mexendo na performance de quatro sites em HTML estático, e o resultado
Não sou programador. Sou corretor de planos de saúde e sócio de uma corretora, e cuido pessoalmente dos nossos sites. Nesta semana passei alguns dias mexendo na performance de quatro sites em HTML estático, e o resultado me surpreendeu: o pior deles saiu de 64 para 97 no PageSpeed Insights mobile.
Vou contar o que fiz, na ordem, com os números reais. Se você tem um site feito a partir de um template pronto (desses com Bootstrap, jQuery e uma dúzia de plugins), é bem provável que o seu tenha os mesmos problemas.
Transparência: fiz esse trabalho com ajuda de IA (Claude), que escreveu os scripts e conferiu cada etapa comigo. As decisões, os testes e os números são dos meus sites.
O incômodo: o WordPress estava mais rápido que o HTML puro
Nosso site principal, de planos da Amil Saúde, roda em WordPress com plugin de cache e marca 94 no mobile. Já os sites menores, feitos em HTML estático, que em teoria deveriam voar, estavam entre 63 e 79.
Não fazia sentido, não é mesmo? HTML puro não tem banco de dados, não tem PHP, não tem nada. O problema não era o tipo de site. Era tudo o que o template carregava sem usar.
O caso deste artigo é um site sobre a linha Amil Black (a linha topo da operadora, que substituiu o antigo Amil One em março de 2026). Ele estava assim no PageSpeed mobile:
- Desempenho: 64
- LCP (Largest Contentful Paint): 7,4 s
Sete segundos para o maior elemento aparecer na tela de um celular. Muita gente desiste antes disso.
Passo 1: descobrir o que o template carregava de graça
Abri o <head> e o fim do <body> da home. Esta era a lista de scripts, carregada em todas as páginas:
<script src="plugins/jQuery/jquery.min.js"></script>
<script src="plugins/bootstrap/bootstrap.min.js"></script>
<script src="plugins/magnific-popup/jquery.magnific.popup.min.js"></script>
<script src="plugins/slick/slick.min.js"></script>
<script src="plugins/filterizr/jquery.filterizr.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=..."></script>
<script src="plugins/google-map/gmap.js"></script>
<script src="plugins/syotimer/jquery.syotimer.js"></script>
<script src="plugins/aos/aos.js"></script>
<script src="plugins/swiper/swiper.min.js"></script>
<script src="js/script.js"></script>
<script src="js/menu-black.js"></script>
O site não tem mapa, não tem carrossel, não tem contador regressivo, não tem galeria com filtro. O único script que realmente fazia alguma coisa era o último, menu-black.js, que é o menu.
No CSS, a mesma história: Bootstrap completo, Slick, Slick Theme, Themify Icons e Font Awesome 4.5 vindo de uma CDN.
Uma curiosidade: em outro dos sites, o script.js do template tinha um trecho de CSS colado no final do arquivo. Isso é erro de sintaxe, então o script nunca executou, provavelmente durante anos, e ninguém percebeu. Era peso puro.
Como tirar sem quebrar nada
Antes de apagar qualquer coisa, tirei print de página inteira de todas as páginas, no desktop (1280 px) e no celular (390 px), com o Playwright. Depois de cada mudança, tirei os prints de novo e comparei pixel a pixel.
Para comparar, usei uma tolerância pequena (diferença maior que 24 em algum canal de cor), porque gradientes de fundo às vezes variam um pouco entre uma renderização e outra e geram "diferença" que o olho não vê.
Também conferi três coisas que print não mostra:
-
Erros de JavaScript no console (
pageerrorno Playwright). - O menu: o mega menu no hover do desktop e o hambúrguer no celular.
- O botão "voltar ao topo".
Esse terceiro item me pegou. O botão dependia do script.js antigo, que era jQuery. Tirei o jQuery e precisei de um substituto. Foram seis linhas de JavaScript puro, no arquivo do menu que já existia:
document.addEventListener('click', function (e) {
var b = e.target.closest ? e.target.closest('.back-to-top') : null;
if (!b) return;
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
Resultado: 11 scripts a menos por página, além dos CSS de Slick, Slick Theme e Font Awesome.
Passo 2: PurgeCSS no Bootstrap
Mesmo sem os plugins, o bootstrap.min.css tinha 141 KB, e o site usa uma fração disso: o grid, alguns utilitários e o collapse do menu.
Usei o PurgeCSS, passando como conteúdo todos os HTML e os JS que sobraram:
import { PurgeCSS } from 'purgecss';
import fs from 'fs';
const result = await new PurgeCSS().purge({
content: ['site/*.html', 'site/js/*.js'],
css: ['site/plugins/bootstrap/bootstrap.min.css'],
safelist: {
standard: ['show', 'collapse', 'collapsing', 'fade', 'active', 'open'],
greedy: [/^modal/, /^dropdown/, /^navbar/, /^nav-/]
}
});
fs.writeFileSync('site/plugins/bootstrap/bootstrap.min.css', result[0].css);
A safelist é a parte importante. Classes que o JavaScript adiciona na hora (como show no menu aberto) não aparecem no HTML, e o PurgeCSS apagaria essas classes se você não avisar.
- Bootstrap: 141 KB → ~26 KB
- Themify Icons: 18,6 KB → ~1 KB (o site usa meia dúzia de ícones)
💡 Dica: depois de purgar, coloque um ?v=4 (ou qualquer número novo) no link do CSS. Sem isso, quem já visitou o site continua com o arquivo antigo em cache e você fica achando que algo deu errado.
Passo 3: a imagem do LCP (aqui estava o 7,4 s)
O maior elemento da primeira tela era a imagem principal da home: um PNG de 399 KB.
Convertida para WebP, com uma versão menor para o celular:
-
amil-black-hero.webp(700 px): 24,9 KB -
amil-black-hero-400.webp(400 px): 11,6 KB
<link rel="preload" as="image" href="images/amil-black-hero.webp"
imagesrcset="images/amil-black-hero-400.webp 400w, images/amil-black-hero.webp 700w"
imagesizes="(max-width: 575px) 88vw, 532px" fetchpriority="high">
<img src="images/amil-black-hero.webp"
srcset="images/amil-black-hero-400.webp 400w, images/amil-black-hero.webp 700w"
sizes="(max-width: 575px) 88vw, 532px"
width="700" height="525" fetchpriority="high" decoding="async"
alt="Mulher sorrindo segurando o cartão do plano Amil Black">
O width e o height evitam que a página "pule" quando a imagem chega (é o CLS). O sizes diz ao navegador que, no celular, a imagem ocupa 88% da largura da tela, então ele baixa a versão de 400 px.
E aqui encontrei o erro mais curioso de todos. Outra imagem, uma foto que fica no meio da página, estava marcada com fetchpriority="high":
<img src="images/amil-black/amil-black.jpg" alt="..." fetchpriority="high">
Ou seja: o navegador baixava com prioridade máxima uma foto que o visitante só vê depois de rolar a tela, competindo com a imagem que realmente importava. Troquei por loading="lazy" e converti essa foto também para WebP, em três tamanhos.
💡 Dica: procure fetchpriority="high" no seu site inteiro. Deve existir no máximo um por página, e só na imagem do LCP.
Aproveitei para reescrever os alt das imagens. Se a imagem tem texto escrito nela (banner, chamada), o alt deve dizer o que está escrito, não só descrever a cena.
Passo 4: as fontes do Google sem travar a página
O style.css do template começava a carregar as fontes assim:
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,700|Poppins:300,400,500,600,700|Work+Sans:400,500,600|Roboto:100,200,300,400,500,600,700,800,900");
Isso é ruim por dois motivos:
-
@importdentro do CSS é uma cadeia: o navegador baixa ostyle.css, só então descobre o@import, só então vai ao Google. Tudo isso antes de pintar o texto. - Quatro famílias e 20 pesos. O site usa duas famílias e três pesos.
Tirei o @import e coloquei no <head> de cada página:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Poppins:wght@400;600;700&display=swap"
media="print" onload="this.media='all'">
<noscript>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&family=Poppins:wght@400;600;700&display=swap">
</noscript>
O truque do media="print" faz o navegador baixar a fonte sem bloquear a renderização. Quando termina, o onload aplica a fonte na tela. O display=swap mostra o texto com a fonte do sistema enquanto isso.
Passo 5: gzip e cache no .htaccess
O servidor é Apache (hospedagem compartilhada comum). Sem nenhuma configuração, ele não comprimia nada e não dizia ao navegador por quanto tempo guardar os arquivos.
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml image/svg+xml font/woff2
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
Tudo dentro de <IfModule>: se o módulo não existir no servidor, o bloco é ignorado e o site não cai com erro 500.
HTML com cache zero é de propósito: assim, quando eu atualizo uma página, o visitante vê a versão nova na hora. Imagens e fontes quase nunca mudam e podem ficar um ano em cache. CSS e JS têm o ?v= para forçar a atualização quando eu mexo neles.
No site de Brasília, só esse passo levou a nota de 78 para 89. Foi o ajuste com melhor custo-benefício de todos.
O resultado
| Métrica (mobile) | Antes | Depois |
|---|---|---|
| Desempenho | 64 | 97 |
| First Contentful Paint | — | 1,1 s |
| Largest Contentful Paint | 7,4 s | 2,6 s |
| Total Blocking Time | — | 0 ms |
| Cumulative Layout Shift | — | 0,001 |
| Speed Index | — | 2,7 s |
| SEO | — | 100 |
Não anotei todas as métricas do "antes", só a nota e o LCP. Fica a lição: tire print completo do PageSpeed antes de começar.
Fiz o mesmo processo nos outros três sites:
O que ainda falta
- LCP de 2,6 s: está um pouco acima do verde, que vai até 2,5 s. No print do PageSpeed, o banner de cookies cobre boa parte da tela do celular, e desconfio que ele esteja sendo contado como o maior elemento. É o próximo teste.
- Acessibilidade em 85: provavelmente é o contraste do texto dourado sobre o fundo preto e a ordem dos títulos. Ajuste fino, mas importa.
Checklist para o seu site
- Liste os scripts e CSS de cada página: para cada um, pergunte "o que quebra se eu tirar?". Em template pronto, a resposta costuma ser "nada".
- Tire prints antes e depois: de todas as páginas, desktop e celular. Compare automaticamente.
-
Purgue o Bootstrap: com uma
safelistpara as classes que o JavaScript adiciona. -
Ataque a imagem do LCP: WebP,
srcset,preloadefetchpriority="high"só nela. -
Procure
fetchpriorityerrado: um por página, no máximo. -
Tire o
@importde fontes do CSS: carregue só as famílias e os pesos que você usa, sem bloquear. -
Configure gzip e cache: sempre dentro de
<IfModule>. -
Coloque
?v=nos arquivos alterados: para o cache não esconder a sua mudança.
Se você tem um site parado em 60 e poucos no mobile, comece pela imagem do LCP e pelo .htaccess. São os dois passos que mais mexeram na nota aqui.
Ficou alguma dúvida, ou você já passou por algo parecido com template pronto? Me conta nos comentários.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.
