Dev.to WebDev 🛠 Dev 👁 0 📖 9 min read

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

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 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.

PageSpeed mobile do site em WordPress: 94 de desempenho, 95 de acessibilidade, 100 em práticas recomendadas e 100 em SEO

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:

  1. Erros de JavaScript no console (pageerror no Playwright).
  2. O menu: o mega menu no hover do desktop e o hambúrguer no celular.
  3. 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:

  1. @import dentro do CSS é uma cadeia: o navegador baixa o style.css, só então descobre o @import, só então vai ao Google. Tudo isso antes de pintar o texto.
  2. 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:

Gráfico de barras do PageSpeed mobile antes e depois em quatro sites HTML estáticos: Amil Black 64 para 97, Brasília 64 para 94, MEI São Paulo 63 para 93 e Jundiaí 79 para 93

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 safelist para as classes que o JavaScript adiciona.
  • Ataque a imagem do LCP: WebP, srcset, preload e fetchpriority="high" só nela.
  • Procure fetchpriority errado: um por página, no máximo.
  • Tire o @import de 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.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.