Page Performance — Skill de Otimização
Overview
Transforma qualquer LP ou página web em uma página com score 90+ no PageSpeed Insights (mobile). Baseada em técnicas validadas empiricamente em dezenas de landing pages reais, com múltiplas iterações e trade-offs documentados.
Resultado esperado: Score 90+ mobile, LCP < 2.5s, CLS < 0.1, FCP < 1.8s.
Quando Usar
- Score PageSpeed mobile abaixo de 90
- LCP > 2.5s ou CLS > 0.1
- Página com imagens pesadas (JPG/PNG sem otimização)
- Google Fonts carregando via link externo (render-blocking)
- CSS monolítico bloqueando renderização
- Antes de lançamento (captação, vendas) — performance impacta conversão
- Após adicionar novas seções/imagens a uma LP existente
Workflow de Otimização (6 Etapas em Ordem de Impacto)
Seguir NESTA ORDEM — cada etapa está ranqueada pelo impacto real medido.
Etapa 1: Diagnóstico
Antes de qualquer mudança, medir o estado atual.
PageSpeed Insights (web):
https://pagespeed.web.dev/analysis?url=https://SEU-DOMINIO.com.brLighthouse CLI (local, mais controle):
npx lighthouse https://SEU-DOMINIO.com.br --output=json --output-path=./report.json --preset=perf --chrome-flags="--headless"Métricas a registrar:
| Métrica | Meta | O que mede |
|---|---|---|
| Performance Score | 90+ | Score geral |
| LCP (Largest Contentful Paint) | < 2.5s | Quando o maior elemento renderiza |
| CLS (Cumulative Layout Shift) | < 0.1 | Quanto a página "pula" |
| FCP (First Contentful Paint) | < 1.8s | Primeiro conteúdo visível |
| Speed Index | < 3.4s | Velocidade de preenchimento visual |
| TBT (Total Blocking Time) | < 200ms | Tempo que JS bloqueia interação |
Anotar: Score atual, maiores oportunidades listadas pelo PageSpeed, tamanho total da página.
Etapa 2: Imagens (Maior Impacto — ~400KB savings)
Imagens são quase sempre o maior gargalo. Uma foto JPG de 500KB pode virar 45KB WebP.
2.1 Converter para WebP
# Requer: pip install Pillow
from PIL import Image
import os
def optimize_image(input_path, max_width, quality=80):
"""Converte JPG/PNG para WebP otimizado."""
img = Image.open(input_path)
# Redimensionar mantendo proporção
if img.width > max_width:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img = img.resize((max_width, new_height), Image.LANCZOS)
# Salvar como WebP
output_path = os.path.splitext(input_path)[0] + '.webp'
img.save(output_path, 'WebP', quality=quality, method=6)
return output_path
# Uso — gerar versão desktop + mobile de cada imagem
optimize_image('fotos/foto-hero.JPG', max_width=800) # Desktop
optimize_image('fotos/foto-hero.JPG', max_width=480) # MobileDimensões recomendadas:
- Hero desktop: max 800px width
- Hero mobile: max 480px width
- Imagens secundárias: max 600px width
- quality=80 é o sweet spot (abaixo disso perde qualidade visível)
2.2 Implementar <picture> com srcset
<!-- HERO (above-fold) — fetchpriority="high" + preload -->
<picture>
<source media="(max-width: 768px)" srcset="fotos/foto-hero-mobile.webp" type="image/webp">
<source srcset="fotos/foto-hero.webp" type="image/webp">
<img src="fotos/foto-hero.JPG" alt="Descrição" width="800" height="1200" fetchpriority="high">
</picture>
<!-- BELOW-FOLD — loading="lazy" -->
<picture>
<source media="(max-width: 768px)" srcset="fotos/foto-about-mobile.webp" type="image/webp">
<source srcset="fotos/foto-about.webp" type="image/webp">
<img src="fotos/foto-about.JPG" alt="Descrição" width="900" height="1348" loading="lazy">
</picture>2.3 Preload da imagem hero
<!-- No <head>, ANTES de qualquer CSS -->
<link rel="preload" as="image" type="image/webp" href="fotos/foto-hero-mobile.webp" media="(max-width: 768px)">
<link rel="preload" as="image" type="image/webp" href="fotos/foto-hero.webp" media="(min-width: 769px)">Regras de imagem:
fetchpriority="high"— APENAS na imagem hero (above-fold)loading="lazy"— em TODAS as imagens below-foldwidtheheightexplícitos — SEMPRE (previne CLS)- Fallback JPG no
<img src>— compatibilidade com browsers antigos
Etapa 3: CSS Split (Segundo Maior Impacto — ~200KB savings em render-blocking)
CSS monolítico bloqueia renderização. Separar em 3 partes:
3.1 Arquitetura de CSS
<head>
├── <style> ... </style> → Critical CSS inline (~6KB max)
├── fonts.css (deferred) → @font-face declarations
└── styles-deferred.css (deferred) → Todo o resto
</head>3.2 Critical CSS (inline no <style>)
Incluir APENAS estilos do above-fold:
- CSS Reset mínimo
- CSS Variables (
:root {--cores, --fontes}) - Body, html base
- Header/navbar
- Hero section completa
- CTA buttons
- Form de captação (se above-fold)
Meta: ~6KB max inline. Acima de 10KB já prejudica Speed Index.
3.3 CSS Deferred (arquivo externo, non-blocking)
<!-- Non-critical CSS -->
<link rel="stylesheet" href="styles-deferred.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="styles-deferred.css"></noscript>O que vai no deferred:
- Seções below-fold (sobre, benefícios, FAQ, footer)
- Animações e transições
- Media queries complexas
- Estados hover/focus
3.4 Minificação
O CSS inline no <style> DEVE ser minificado (uma linha, sem espaços extras). Remover comentários, quebras de linha e propriedades duplicadas.
Etapa 4: Fontes (~1.750ms savings em render-blocking)
Google Fonts via <link> externo é render-blocking. Self-host elimina isso.
4.1 Baixar fontes
- Acessar Google Fonts API com user-agent de browser moderno para obter woff2
- Baixar APENAS subsets
latinelatin-ext(suficiente para PT-BR) - Salvar em pasta
/fonts/
4.2 Criar fonts.css
Regras críticas:
font-display: optional— NUNCAswap. Optional = zero CLS, browser usa fallback se fonte não carregou a tempo. Swap causa flash visível.unicode-range— subsetting automático, browser só baixa o que precisa- Formato
woff2apenas — suporte universal em browsers modernos
4.3 Carregar fonts.css (deferred)
<link rel="preload" as="style" href="fonts.css">
<link rel="stylesheet" href="fonts.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="fonts.css"></noscript>Ordem no <head>:
- Preload fonts.css
- Preload hero image
- Critical CSS inline
<style> - Deferred fonts.css (media="print")
- Deferred styles.css (media="print")
Etapa 5: Cache Headers (Revisitas — +120KB savings)
Para Vercel, configurar vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-XSS-Protection", "value": "1; mode=block" }
]
},
{
"source": "/fotos/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/fonts/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/(.*).html",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }
]
}
]
}Lógica (3 níveis de cache):
- Imutáveis (imagens, fonts, CSS):
immutable, 1yr— nunca mudam sem novo filename - Dinâmicos (config, JS):
must-revalidate— muda entre deploys - HTML:
must-revalidate— sempre busca versão mais recente - Security headers em tudo
Etapa 6: JavaScript
6.1 Defer em scripts externos
<script src="config.js?v=2" defer></script>- Nunca usar
asyncpara scripts que dependem de ordem.defermantém ordem e não bloqueia parsing. - Usar
?v=Ncomo cache-busting quando o config muda.
6.2 NUNCA inline JS grande no <head>
Mover config/inicialização para arquivo externo com defer. JS inline no <head> aumenta o tamanho do HTML, que impacta TUDO no PageSpeed throttled.
6.3 Ordem de Inicialização com defer + inline script (CRÍTICO)
Quando há um <script src="config.js" defer> seguido de um <script> inline, o inline executa ANTES do defer. Variáveis definidas no config.js NÃO existem quando o inline roda.
Regra: NUNCA chamar funções que dependem de config.js no top-level do inline script. Sempre dentro de DOMContentLoaded.
<script src="config.js?v=2" defer></script>
<script>
// 1. Definir funções (não dependem de config.js)
function initPage() { /* usa LP_CONFIG */ }
// 2. Registrar via DOMContentLoaded (config.js já terá executado)
document.addEventListener('DOMContentLoaded', initPage);
// 3. TODO código que usa variáveis do config → dentro de DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
// Countdown, form submit, smooth scroll, etc.
});
</script>6.4 Facebook Pixel deferred
<!-- Injetar via JS após page load para não bloquear renderização -->
<script>
window.addEventListener('load', () => {
// Inicializar pixel aqui
});
</script>Armadilhas Comuns (Gotchas)
font-display: swap com size-adjust = CLS PIOR
- Solução:
font-display: optional. Browser usa fallback se fonte não carregou — zero CLS garantido.
Inline de TODO o CSS = Speed Index piora
- HTML fica 80KB+. No throttled 4G (1.6 Mbps, 150ms RTT), HTML grande = TUDO mais lento.
- Solução: Apenas ~6KB de critical CSS inline. Resto em arquivo deferred.
immutable em JS que muda = configs não atualizam
- Solução: Arquivos que mudam entre deploys =
must-revalidate.immutableAPENAS para assets que nunca mudam.
defer + inline script = variáveis não existem no top-level
- O inline executa ANTES do defer. Se acessar variáveis do config.js no top-level → TypeError → crash silencioso.
- Solução: TODO código que depende de config.js dentro de
DOMContentLoaded.
Regra de Ouro do PageSpeed
No ambiente throttled do PageSpeed (4G: 1.6 Mbps, 150ms RTT), o tamanho do HTML impacta TODAS as métricas. Cada KB a mais no HTML atrasa FCP, LCP, Speed Index e TBT. Menor HTML = melhor score.
Checklist de Verificação
Imagens
- Todas convertidas para WebP (quality=80)
- Versões mobile (max 480px) e desktop (max 800px)
<picture>com<source>WebP + fallback JPGfetchpriority="high"APENAS na hero imageloading="lazy"em TODAS as imagens below-foldwidtheheightexplícitos em TODAS as<img>- Preload da hero image no
<head>
CSS
- CSS separado: critical inline + deferred externo + fonts
- Critical CSS < 6KB (apenas above-fold, minificado)
- Deferred CSS via
media="print" onload="this.media='all'" - Nenhum
transition: all(especificar propriedades)
Fontes
- Google Fonts self-hosted (woff2)
font-display: optional(NUNCA swap)- fonts.css carregado como deferred
JavaScript
- Scripts externos com
defer - Nenhum JS grande inline no
<head> - Código que depende de config.js dentro de
DOMContentLoaded - Facebook Pixel carregado após
window.load
Cache & Deploy
- vercel.json com 3 níveis de cache
- Security headers configurados
?v=Ncache-busting em scripts que mudam
Resultado Final
- Score PageSpeed mobile 90+
- LCP < 2.5s
- CLS < 0.1
- FCP < 1.8s
- Testar em desktop E mobile
Estrutura de Referência (LP Otimizada)
lp-otimizada/
├── index.html → HTML com critical CSS inline (~30-35KB)
├── obrigado.html → Página obrigado (mesmas otimizações)
├── styles-deferred.css → CSS non-critical (~12KB)
├── fonts.css → @font-face declarations (~7KB)
├── config.js → Configuração + pixel + init (defer)
├── fonts/ → woff2 self-hosted
├── fotos/ → WebP otimizado (desktop + mobile)
├── favicon.svg → Ícone SVG (< 1KB)
└── vercel.json → Cache headers + security + rewritesOrdem dos elementos no <head>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>...</title>
<meta name="description" content="...">
<!-- 1. Preloads (fontes e hero image) -->
<link rel="preload" as="style" href="fonts.css">
<link rel="preload" as="image" type="image/webp" href="fotos/hero-mobile.webp" media="(max-width: 768px)">
<link rel="preload" as="image" type="image/webp" href="fotos/hero.webp" media="(min-width: 769px)">
<!-- 2. Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<!-- 3. Fonts deferred -->
<link rel="stylesheet" href="fonts.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="fonts.css"></noscript>
<!-- 4. Critical CSS inline (APENAS above-fold, ~6KB max) -->
<style>
/* Reset + variables + hero + header + CTA + form */
</style>
<!-- 5. Non-critical CSS deferred -->
<link rel="stylesheet" href="styles-deferred.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="styles-deferred.css"></noscript>
<!-- 6. OG/Twitter meta tags -->
<meta property="og:title" content="...">
</head>