Todo mundo concorda que site lento perde venda. Poucos times conseguem dizer, com número na mão, onde o tempo se perde, o que consertar primeiro e como impedir que o ganho evapore na próxima entrega. É nesse ponto que performance front-end deixa de ser preferência de desenvolvedor e passa a ser assunto de diretoria. Este post é o roteiro prático que usamos, na ordem em que ele costuma dar mais retorno.
Um aviso útil antes de começar: quase tudo aqui é plataforma web, não framework. Os exemplos rodam igual em React, Angular, Vue ou em uma aplicação sem framework algum, porque quem paga a conta do tempo é o navegador, não a biblioteca escolhida. Onde a sintaxe muda de fato, mostramos o equivalente nos três principais.
Passo 1: medir com dado de campo, não com nota de laboratório
Três métricas resumem a experiência percebida. LCP mede quando o conteúdo principal aparece, e a faixa boa termina em 2,5 segundos. INP mede o atraso entre o clique e a resposta da tela, com alvo em 200 milissegundos. CLS mede o quanto o layout pula durante o carregamento, e o teto é 0,1.
Nota de auditoria rodada no seu notebook, em fibra, diz pouco. O que conta é o dado do usuário real, com a rede e o aparelho que ele tem. Instrumentar isso leva alguns minutos:
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals';
function enviar({ name, value, rating, id }) {
const corpo = JSON.stringify({
metrica: name, valor: value, faixa: rating, id,
rota: location.pathname,
conexao: navigator.connection?.effectiveType
});
navigator.sendBeacon('/api/vitals', corpo);
}
[onLCP, onINP, onCLS, onTTFB].forEach(registrar => registrar(enviar));Guarde isso em uma tabela com rota, data e percentil. O número que importa é o p75 por rota, não a média: a média esconde justamente o cliente que desistiu. Complete com o PageSpeed Insights, que já traz o histórico de campo do seu domínio, e com o painel Performance do navegador para investigar caso a caso.
Sem esse passo, todo o resto é chute caro.
Passo 2: abrir o pacote e cortar o que não paga o próprio peso
Na maioria dos projetos, o vilão é JavaScript. Comece olhando o que existe de fato dentro do pacote:
# funciona com qualquer bundler que gere source map
npx source-map-explorer 'dist/**/*.js' # Vite, esbuild, Rollup
npx source-map-explorer 'build/static/js/*.js' # Webpack, CRA
npx source-map-explorer 'dist/*/main*.js' # Angular CLITrês padrões aparecem quase sempre. O primeiro é a biblioteca importada por inteiro para resolver um detalhe, como formatação de data, quando a plataforma já resolve de graça:
// dezenas de kB no pacote inicial
format(data, 'dd/MM/yyyy');
// nativo, zero kB adicionais
new Intl.DateTimeFormat('pt-BR').format(data);O segundo é o componente pesado que entra no primeiro carregamento e só é usado por poucos usuários, em telas internas. Editor de texto rico, biblioteca de gráficos, gerador de PDF, mapa. Todos deveriam entrar sob demanda. A ferramenta que resolve isso é do próprio JavaScript, o import() dinâmico, que qualquer bundler entende e transforma em um arquivo separado:
// antes: o editor viaja no pacote inicial de toda visita
import { iniciarEditor } from './rich-editor';
// depois: o arquivo só é baixado nesta linha, quando o formulário abre
const { iniciarEditor } = await import('./rich-editor');Cada framework embala essa mesma chamada de um jeito, e o efeito na rede é idêntico:
// React
const RichEditor = lazy(() => import('./rich-editor'));
// Vue
const RichEditor = defineAsyncComponent(() => import('./RichEditor.vue'));
// Angular, na definição de rota
{ path: 'editor', loadComponent: () => import('./rich-editor.component')
.then(m => m.RichEditorComponent) }O terceiro é a ausência de divisão por rota. Se a página de produto baixa o código do checkout, do painel administrativo e do relatório, o cliente paga por telas que talvez nunca veja. Divisão por rota é ajuste de configuração no bundler, não reescrita.
Em negócio, esse passo é o de melhor relação entre esforço e resultado: quase sempre é onde estão os primeiros segundos recuperados.
Passo 3: consertar a cascata de requisições
Depois do peso vem a ordem. O problema clássico é a cascata: o HTML chega, ele pede o JavaScript, o JavaScript pede a configuração, a configuração pede os dados, e só então a tela desenha. Cada elo adiciona uma ida e volta de rede.
O que resolve, em ordem de impacto: buscar dados no servidor junto com a página, em vez de esperar o navegador pedir depois; disparar chamadas independentes em paralelo com Promise.all, em vez de encadeadas em await sequencial; e avisar o navegador com antecedência sobre o que ele vai precisar.
<link rel="preconnect" href="https://api.seudominio.com.br">
<link rel="preload" as="image" fetchpriority="high" href="/hero.avif">Uma armadilha que aparece com frequência: a imagem principal da dobra marcada como loading="lazy". Ela é o seu LCP, e o atributo está pedindo ao navegador que a deixe para depois. Lazy loading vale para o que está abaixo da dobra, nunca para o herói.
Passo 4: imagens e fontes, o ganho mais barato do lote
Imagem mal servida é o item que mais pesa em página de conteúdo e de catálogo. O tratamento completo cabe em uma tag:
<img src="/produto-800.avif"
srcset="/produto-400.avif 400w, /produto-800.avif 800w, /produto-1600.avif 1600w"
sizes="(max-width: 768px) 100vw, 50vw"
width="800" height="600"
alt="Cadeira ergonômica vista de perfil"
loading="lazy" decoding="async">Formato moderno reduz o peso, srcset evita entregar 1600 pixels para uma tela de celular, e width com height reserva o espaço antes do download, o que derruba o CLS. Faltando essas duas medidas, o texto pula quando a imagem chega, e o usuário clica no lugar errado.
Fonte é a mesma história com outra roupa. Auto-hospede, gere um subconjunto apenas com os caracteres que o site usa, declare font-display: swap para o texto aparecer imediatamente com a fonte do sistema, e faça preload apenas do peso usado na primeira dobra.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin-subset.woff2') format('woff2');
font-weight: 400 700;
font-display: swap;
}Passo 5: domar os scripts de terceiros
Pixel de anúncio, gerenciador de tags, chat de atendimento, teste A/B, mapa de calor. Cada um entrou por uma decisão legítima, e o conjunto virou o maior consumidor de tempo da página sem que ninguém revisasse a lista.
O procedimento é direto. Monte o inventário, com dono e propósito de cada script, e remova o que não tem dono. Para o que fica, mude a estratégia de carregamento. Chat de atendimento não precisa existir antes do primeiro clique:
const botao = document.querySelector('#abrir-chat');
const carregarChat = () => import('./chat-widget').then(m => m.iniciar());
botao.addEventListener('click', carregarChat, { once: true });O botão é um desenho estático, barato, e o widget de verdade só desce quando alguém demonstra intenção. A mesma técnica vale para vídeo incorporado, que pode começar como imagem de capa clicável.
Aqui a conversa deixa de ser técnica e passa a ser de governança: alguém precisa ter autoridade para dizer não a mais uma tag. Sem isso, o time otimiza em um trimestre e devolve o ganho no seguinte.
Performance front-end começa na escolha de renderização
Se depois de tudo isso o número ainda não fecha, o problema é arquitetural. Conteúdo público que precisa ser encontrado pede renderização no servidor ou geração estática, com streaming para enviar o que já está pronto em vez de aguardar a página inteira. Aplicação interna autenticada, com sessão longa e muita interação, convive bem com uma abordagem mais próxima do cliente, com hidratação parcial só no que é realmente interativo.
É uma decisão com efeito direto em custo de infraestrutura e em tempo de resposta, e trocá-la depois é caro. Por isso ela pertence ao começo do projeto, não a uma força-tarefa de otimização seis meses após o lançamento.
Passo 6: travar o ganho com orçamento de performance no pipeline
Ganho de performance não se sustenta sozinho. O que segura é transformar velocidade em limite verificável, do mesmo jeito que um teste quebrado impede o merge:
{
"ci": {
"collect": {
"url": ["https://staging.seudominio.com.br/", "https://staging.seudominio.com.br/checkout"],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"total-byte-weight": ["warn", { "maxNumericValue": 1000000 }]
}
}
}
}No laboratório não existe INP, então use o total blocking time como aproximação e confirme o INP real no dado de campo do passo 1. Some a isso um teto de peso para o pacote inicial verificado no build, e um alerta quando o p75 de produção piorar mais de dez por cento entre versões. Versionamento e rollback fecham o ciclo: quando uma dependência nova degrada a experiência, você volta em minutos, sem discussão.
Um plano de trinta dias que costuma funcionar
Na Alcance Tech, quando entramos em um projeto com esse sintoma, a sequência é sempre a mesma. Na primeira semana, instrumentar o dado de campo e escolher as cinco rotas que carregam receita, tipicamente entrada de campanha, busca, produto, cadastro e checkout, com uma linha de base registrada. Na segunda, pacote e terceiros, que é onde o ganho aparece mais rápido. Na terceira, imagem, fonte, cascata e política de cache. Na quarta, orçamento no pipeline, alerta de regressão e, se o alvo ainda não fechou, a conversa de renderização.
O resultado desse recorte é que o time trabalha no que aparece no funil, e não em uma lista infinita de melhorias tecnicamente elegantes que nenhum cliente percebe.
Velocidade de interface não é capricho estético. É a primeira promessa que o seu produto faz ao cliente, e a mais fácil de quebrar sem que ninguém no time perceba.





