Pular para o conteúdo
Gustavo Tutorial

Qualidade

Esta trilha é sobre o que separa uma página que funciona de uma página que funciona bem: o peso da imagem, quem consegue usar, quanto demora para aparecer, e o que se confere antes de mandar o link para o cliente.

Revisão já conversamos sobre isso Novo ainda não vimos 6 partes · 40 tópicos

Nenhum tópico com esse filtro.

PARTE 1

Imagens e mídia

É onde está 80% do peso de uma landing page e a maior parte da diferença entre um site que abre em um segundo e um que abre em oito. Também é onde mora o risco jurídico mais bobo do ofício: usar foto que não é sua.

O que realmente pesa numa página

Novo

Abra a aba Network de qualquer landing page e ordene por tamanho. A ordem é quase sempre a mesma: imagem, imagem, imagem, fonte, JavaScript, CSS, HTML. O HTML — o arquivo que você passou o dia escrevendo — costuma ser o menor de todos.

RecursoPeso típico mal feitoPeso bem feito
Foto de hero3 a 6 MB (direto da câmera ou do celular)80 a 150 KB
Galeria de 8 fotos20 MB500 KB
Fontes400 KB (6 pesos de 2 famílias)60 KB (2 pesos)
HTML da página—30 a 80 KB

A conclusão prática é dura e libertadora: você pode escrever o CSS mais elegante do mundo que não muda nada se a foto da hero tem 4 MB. Otimizar imagem é a única tarefa de performance que vale fazer antes de qualquer outra.

A conta que convence o cliente: 4G brasileiro real entrega algo perto de 2 MB por segundo em condição boa, bem menos em movimento. Uma página de 8 MB leva mais de dez segundos. Metade dos visitantes desiste antes de três.

Qual formato usar

Novo
FormatoUse paraObservação
AVIFFoto, hoje, como primeira escolhaMenor de todos — 30 a 50% abaixo do WebP. Suportado em todos os navegadores atuais.
WebPFoto, como alternativa de segurançaSuporte praticamente universal há anos. Se só puder escolher um, escolha este.
JPGÚltimo fallbackAinda pesa 30% mais que WebP na mesma qualidade.
PNGSó quando precisa de transparência duraPara foto, é o pior dos mundos: enorme. Logo em PNG quase sempre deveria ser SVG.
SVGLogo, ícone, ilustração geométricaEscala em qualquer tela sem perder nitidez e costuma ter poucos KB. É texto — pode editar cor no próprio código.
GIFNuncaPara animação curta, um <video> em MP4 pesa 10 vezes menos.
Regra prática: foto vira AVIF com WebP de reserva. Logo e ícone viram SVG. Se o cliente mandou o logo em JPG com fundo branco, peça o vetor — todo designer que fez aquele logo tem o arquivo .ai ou .svg. Vale insistir: logo pixelado no topo da página é a primeira coisa que derruba a percepção de profissionalismo.

Comprimir — alvos e ferramentas

Novo

Antes de comprimir, redimensione. Uma foto de 6000px de largura numa área que exibe 1200px é desperdício puro, e nenhuma compressão conserta isso.

UsoLargura máximaAlvo de peso
Hero de tela cheia1920px (e uma versão de 960px para celular)até 150 KB
Foto dentro de seção1200pxaté 100 KB
Card / galeria800pxaté 60 KB
Avatar, ícone200pxaté 15 KB

Sem instalar nada: Squoosh (squoosh.app) faz uma imagem por vez mostrando antes e depois lado a lado — bom para calibrar o olho. Qualidade 75 a 80 em AVIF/WebP é indistinguível do original em quase toda foto.

No terminal, para o lote inteiro de uma vez:

terminal
# ImageMagick — redimensiona e converte a pasta toda
magick mogrify -resize 1200x -quality 80 -format webp *.jpg

# Sharp (Node) — mais rápido e melhor qualidade
npx @squoosh/cli --avif '{"quality":75}' --resize '{"width":1200}' fotos/*.jpg

# conferir o resultado
ls -lh *.webp
O hábito que resolve: nenhuma imagem entra no projeto sem passar por isso. Crie a pasta fotos-brutas/ fora do repositório (está no .gitignore), processe de lá para dentro, e o que vai para o Git já está pronto.

srcset e sizes — a imagem certa para cada tela

NovoHTML

Mandar a imagem de 1920px para um celular de 390px é mandar quatro vezes mais pixel do que a tela consegue mostrar. O srcset deixa o navegador escolher.

html
<img
  src="hero-1200.webp"
  srcset="hero-600.webp 600w,
          hero-1200.webp 1200w,
          hero-1920.webp 1920w"
  sizes="(max-width: 700px) 100vw, 1200px"
  width="1200" height="800"
  alt="Sala de estar reformada com marcenaria clara e iluminação embutida"
  fetchpriority="high">

O srcset lista as versões com a largura real em pixels (w). O sizes avisa qual espaço a imagem vai ocupar — é a informação que falta para o navegador decidir, e é o que quase todo mundo esquece de escrever.

Não exagere: três versões cobrem tudo. Gerar sete tamanhos é trabalho que não se paga. E se a imagem sempre aparece pequena — um avatar, um ícone de serviço — uma versão só basta.

Quando carregar — lazy, priority e o pulo do layout

NovoHTML
html
<!-- a imagem da hero: aparece de cara, tem que vir primeiro -->
<img src="hero.webp" width="1200" height="800" alt="..." fetchpriority="high">

<!-- tudo que está abaixo da dobra -->
<img src="obra-3.webp" width="800" height="600" alt="..."
     loading="lazy" decoding="async">
Erro clássico: colocar loading="lazy" na imagem da hero. Ela é justamente a que o navegador deveria buscar primeiro — atrasá-la piora diretamente a nota de LCP. Lazy é para o que está fora da tela, e só.

E o width com height em toda imagem: sem eles, o navegador não sabe quanto espaço reservar, a página monta, a imagem chega e tudo pula para baixo. É o CLS — a métrica que mede exatamente esse salto. Os dois números não precisam ser o tamanho final na tela; precisam ser a proporção correta, e o CSS cuida do resto:

css
img { max-width: 100%; height: auto; display: block; }

/* quando a área tem tamanho fixo e a foto precisa preencher */
.card__foto { aspect-ratio: 4 / 3; object-fit: cover; }

<picture> — formato moderno com reserva

NovoHTML

O navegador lê os <source> de cima para baixo e usa o primeiro que entende. O <img> no final é obrigatório e é o que sempre funciona.

html
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" width="1200" height="800" alt="..." fetchpriority="high">
</picture>

<!-- e o outro uso: foto DIFERENTE por tamanho de tela -->
<picture>
  <source media="(max-width: 700px)" srcset="hero-vertical.webp">
  <img src="hero-horizontal.webp" width="1600" height="900" alt="...">
</picture>
Esse segundo caso é art direction, e resolve um problema real: a foto panorâmica que funciona no desktop vira uma faixa ilegível no celular. Em vez de espremer, você troca por um recorte vertical da mesma cena.

De onde vem a foto — e o risco real

Novo

Pegar imagem no Google e colocar no site do cliente é a prática mais comum e mais arriscada do mercado. Existem empresas que fazem busca reversa automatizada e cobram — a cobrança chega para o cliente, e a conversa sobre quem colocou aquilo ali chega para você.

FontePode usar comercialmente?Atenção
Unsplash, Pexels, PixabaySim, gratuitoSem exclusividade — o concorrente pode usar a mesma. Cuidado com foto que tem rosto identificável ou marca visível.
Bancos pagos (Shutterstock, Adobe Stock)Sim, conforme a licença compradaA licença costuma ser por projeto. Guarde o comprovante.
Google ImagensNãoBuscador não é banco de imagem. "Estava na internet" não é licença.
Instagram do clienteDependeSe o cliente tirou, sim. Se foi fotógrafo contratado, o direito costuma ser do fotógrafo — pergunte.
Imagem gerada por IAEm geral simRegras mudam por ferramenta e por país. Nunca use para simular obra que o cliente não fez.
A regra ética que vale mais que a jurídica: foto de banco em site de serviço é provisória, para o cliente ver o layout. Obra de arquitetura que não é do arquiteto, prato que não é do restaurante, sala que não é da clínica — isso é o visitante sendo enganado. Combine desde o orçamento: as fotos definitivas são responsabilidade do cliente, e entram antes de publicar.

Favicon e imagem de compartilhamento

NovoHTML

Duas imagens que ninguém lembra e que aparecem em todo lugar: o ícone da aba e a miniatura que aparece quando o link é colado no WhatsApp.

no head
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"><!-- 180x180 -->

<meta property="og:title" content="Vanessa Carvalho · Arquitetura em Campo Grande">
<meta property="og:description" content="Projetos de interiores e reforma de apartamento, do projeto à entrega.">
<meta property="og:image" content="https://site.com.br/og.jpg"><!-- 1200x630 -->
<meta property="og:url" content="https://site.com.br/">
<meta name="twitter:card" content="summary_large_image">
Três detalhes que quebram na prática: a og:image precisa de URL absoluta (com https://), precisa ser JPG ou PNG — WhatsApp e Facebook ainda engasgam com WebP —, e precisa pesar menos de 300 KB. Teste colando o link no seu próprio WhatsApp antes de entregar; se a miniatura não aparecer, o cliente vai notar antes de você.
PARTE 2

Acessibilidade

Um site acessível é um site que funciona para quem não enxerga bem, não usa mouse, não ouve, ou está com o braço quebrado. No Brasil também é lei. E, para o seu trabalho, é o argumento de venda que nenhum concorrente de orçamento está usando.

Por que importa — inclusive juridicamente

Novo

Cerca de um quarto da população brasileira declara algum tipo de deficiência, e a maior parte é visual. Some quem tem 60 anos e presbiopia, quem está no sol com o brilho baixo, quem quebrou o braço direito. Acessibilidade não é caso de nicho — é o caso comum em condição ruim.

O lado legal: a Lei Brasileira de Inclusão (13.146/2015) determina que sites de empresas com sede ou representação comercial no país sejam acessíveis. Para órgãos públicos existe o eMAG, que é obrigatório. O padrão técnico que todo mundo usa como referência é o WCAG, nível AA.

Como isso vira venda: na proposta, "o site é entregue com acessibilidade conforme WCAG AA, atendendo à Lei Brasileira de Inclusão" é uma linha que o concorrente que cobra R$ 400 não escreve. Para cliente que é clínica, escritório de advocacia ou qualquer negócio que atende idoso, isso pesa — e o custo para você é quase zero se for feito desde o começo.

Semântica: o HTML certo já é metade do caminho

RevisãoHTML

Leitor de tela navega pela estrutura, não pela aparência. Se a estrutura existe, ele funciona sozinho. Se tudo é <div>, ele lê uma sopa sem começo nem fim.

html
<!-- inacessível: o leitor de tela não sabe o que é nada disso -->
<div class="topo"><div class="menu">...</div></div>
<div class="titulo-grande">Arquitetura em Campo Grande</div>
<div class="botao" onclick="abrir()">Orçamento</div>

<!-- acessível: mesma aparência, estrutura declarada -->
<header><nav aria-label="Principal">...</nav></header>
<main>
  <h1>Arquitetura em Campo Grande</h1>
  <button type="button">Orçamento</button>
</main>
<footer>...</footer>

A hierarquia de títulos é a outra metade: um <h1> por página, e nada de pular de h2 para h4 porque ficou do tamanho certo. Tamanho é trabalho do CSS; o número do título é informação de estrutura.

Div com onclick é o pecado mais comum. Um <button> de verdade já recebe foco pelo Tab, já dispara com Enter e Espaço, e já se anuncia como botão. Refazer tudo isso na mão com ARIA dá dez linhas — usar a tag certa dá zero.

Teclado e foco visível

NovoCSS

Teste de trinta segundos que revela quase tudo: abra sua página e navegue só com Tab. Você deveria conseguir chegar em todo link, todo botão e todo campo, sempre vendo onde está.

css
/* NUNCA faça isso — é o crime número um de acessibilidade */
*:focus { outline: none; }

/* faça isso: um foco visível que combina com o site */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* e o pulo do gato para site com cabeçalho fixo */
html { scroll-padding-top: 90px; }

:focus-visible é a solução do dilema antigo: ele mostra o contorno para quem navega por teclado e não mostra para quem clicou com o mouse. Era exatamente isso que as pessoas tentavam resolver apagando o outline.

Link de pular para o conteúdo: quem usa teclado tem que passar pelos dez itens do menu em toda página. Um link escondido, que aparece ao receber foco, resolve — e leva quatro linhas de CSS:
html + css
<a class="pular" href="#conteudo">Pular para o conteúdo</a>

.pular { position:absolute; left:-9999px; }
.pular:focus { left:8px; top:8px; z-index:999; background:#fff;
               padding:10px 14px; border:2px solid currentColor; }

Texto alternativo que serve para alguma coisa

Novo

A regra é uma pergunta: se eu tivesse que ler esta página por telefone para alguém, o que eu diria sobre esta imagem?

html
<!-- inútil -->
<img src="img1.jpg" alt="imagem">
<img src="img1.jpg" alt="foto reforma apartamento campo grande arquitetura">

<!-- útil -->
<img src="sala.jpg" alt="Sala integrada com cozinha, marcenaria clara e
     bancada de madeira maciça separando os ambientes">

<!-- decorativa: alt vazio, de propósito -->
<img src="textura.svg" alt="">

<!-- imagem que É um link -->
<a href="/"><img src="logo.svg" alt="Vanessa Carvalho, página inicial"></a>
SituaçãoO que escrever
Foto que informaDescreva o que importa na foto, em uma frase
Decoração puraalt="" — vazio, mas presente. Sem o atributo, o leitor lê o nome do arquivo.
LogoO nome da empresa
Ícone ao lado de textoalt="" — o texto já diz
Imagem com texto dentroTranscreva o texto. Melhor ainda: não coloque texto dentro de imagem.
Não comece com "imagem de" ou "foto de" — o leitor de tela já anuncia que é uma imagem. E não repita a legenda que está logo abaixo: a pessoa ouve duas vezes.

Contraste medido, não achado

NovoCSS

Cinza claro sobre branco é a assinatura visual do site que ninguém consegue ler no celular no sol. O padrão WCAG AA tem números:

ElementoContraste mínimo
Texto normal4.5:1
Texto grande (24px, ou 19px em negrito)3:1
Borda de campo, ícone que informa3:1
Texto sobre foto4.5:1 — contra a parte mais clara da foto

Onde medir: as próprias DevTools mostram a razão ao clicar no quadradinho de cor no painel Styles, com um aviso quando reprova. Para conferir uma paleta inteira, o WebAIM Contrast Checker.

css
/* texto sobre foto: nunca confie na foto */
.hero { position: relative; }
.hero::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,.65), rgba(0,0,0,.15));
}
.hero__texto { position: relative; z-index: 1; color: #fff; }
E o detalhe que quase todo site erra: placeholder de campo em cinza claro. Ele é texto, precisa dos mesmos 4.5:1 — e, mesmo legível, placeholder não substitui <label>, porque some assim que a pessoa começa a digitar.

ARIA — e quando não usar

NovoHTML

A primeira regra oficial do ARIA é: não use ARIA. Se existe um elemento HTML que faz aquilo, use o elemento. ARIA só descreve — não adiciona comportamento nenhum.

html
<!-- errado: parece botão, mas não recebe Tab nem responde a Enter -->
<div role="button" onclick="enviar()">Enviar</div>

<!-- certo -->
<button type="button" onclick="enviar()">Enviar</button>

Os poucos atributos que você realmente vai usar em site de serviço:

html
<!-- botão sem texto visível -->
<button aria-label="Abrir menu"><svg aria-hidden="true">...</svg></button>

<!-- diferenciar navs quando há mais de uma -->
<nav aria-label="Principal">  <nav aria-label="Rodapé">

<!-- estado que muda -->
<button aria-expanded="false" aria-controls="painel">Dúvidas</button>

<!-- decoração que o leitor deve ignorar -->
<span aria-hidden="true">→</span>

<!-- mensagem que aparece depois e precisa ser anunciada -->
<p role="status">Mensagem enviada.</p>
ARIA errado é pior que ARIA nenhum. Um aria-expanded="false" que nunca vira true faz o leitor de tela anunciar o oposto do que está acontecendo. Se você não vai manter o atributo sincronizado no JavaScript, não o coloque.

Testar em quinze minutos

Novo

Ferramenta automática pega só uns 30% dos problemas — mas são 30% de graça, e a parte manual é rápida.

PassoComoTempo
Auditoria automáticaLighthouse, aba Acessibilidade. Ou a extensão axe DevTools, que é mais detalhada.2 min
Só tecladoTab do topo ao fim. Chega em tudo? Vê onde está? Consegue sair do menu com Esc?3 min
Zoom 200%Ctrl + até 200%. Nada pode sumir nem exigir rolagem horizontal.2 min
Leitor de telaNVDA no Windows (grátis) ou VoiceOver no Mac (Cmd+F5). Ouça só a hero e o formulário.5 min
Sem corDevTools → Rendering → simular acromatopsia. A informação continua clara?1 min
console — auditoria rápida
$$('img').filter(i => !i.hasAttribute('alt'))          // imagem sem alt
$$('a').filter(a => !a.textContent.trim() && !a.getAttribute('aria-label'))
$$('input').filter(i => !i.labels?.length && !i.getAttribute('aria-label'))
document.querySelectorAll('h1').length                  // deve ser 1
$$('button').filter(b => !b.textContent.trim() && !b.getAttribute('aria-label'))
A primeira vez que você ouvir sua própria página num leitor de tela é desconfortável e vale por um curso inteiro. Cinco minutos, uma vez, e você nunca mais escreve alt="imagem".
PARTE 3

Performance medida

Performance não é sensação, é três números que o Google publica e usa para ranquear. Saber o nome de cada um e o que conserta cada um transforma "o site está lento" em uma lista de tarefas.

Core Web Vitals — os três números

Novo
MétricaMedeBomRuim
LCP
Largest Contentful Paint
Quando a maior coisa da tela terminou de aparecer — geralmente a foto ou o título da heroaté 2,5sacima de 4s
INP
Interaction to Next Paint
Quanto a página demora para responder a um clique ou toqueaté 200msacima de 500ms
CLS
Cumulative Layout Shift
Quanto a página pula sozinha enquanto carregaaté 0,1acima de 0,25

Traduzindo para o que o visitante sente: LCP é "demorou para aparecer", INP é "travou quando cliquei", CLS é "eu ia clicar e o botão fugiu".

O INP substituiu o antigo FID em 2024 e é bem mais exigente, porque mede todas as interações da visita, não só a primeira. Em site de HTML puro ele costuma estar ótimo sem esforço nenhum — é uma vantagem sua que vale citar em proposta.

Consertar o LCP

Novo

Primeiro descubra qual elemento é o LCP: rode o Lighthouse e ele aponta na seção de diagnóstico. Em landing page é quase sempre a foto da hero ou o <h1>.

html
<!-- 1. avise o navegador antes mesmo de ele achar a imagem -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">

<!-- 2. nunca lazy na hero, sempre prioridade alta -->
<img src="hero.webp" fetchpriority="high" width="1200" height="800" alt="...">

<!-- 3. conexão antecipada com o que vem de fora -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
CausaConserto
Imagem da hero pesadaComprimir e servir AVIF/WebP — resolve a maioria dos casos sozinho
Imagem com loading="lazy"Tirar o lazy da hero
Fonte bloqueando o textofont-display: swap e preload da fonte principal
CSS gigante no caminho críticoEm HTML único isso não existe; em site com muitos CSS, carregar o resto depois
Servidor lentoHospedagem com CDN. Vercel, Netlify e Cloudflare Pages resolvem de graça.

Consertar o CLS

NovoCSS

CLS é o mais fácil de zerar e o mais irritante para quem visita. Quatro causas cobrem praticamente todos os casos.

css
/* 1. imagem sem dimensão → sempre width e height no HTML */
img, video { max-width: 100%; height: auto; }

/* 2. área que recebe conteúdo depois → reserve o espaço antes */
.mapa { aspect-ratio: 16 / 9; }
.depoimentos { min-height: 320px; }

/* 3. fonte que troca e muda a altura do texto */
@font-face {
  font-family: 'Bricolage';
  src: url(...) format('woff2');
  font-display: swap;
  size-adjust: 100%;      /* ajuste fino para a fonte de reserva ocupar o mesmo espaço */
}

/* 4. banner/aviso que empurra a página → sobreponha, não empurre */
.aviso-cookie { position: fixed; bottom: 0; left: 0; right: 0; }
Como ver o pulo acontecendo: DevTools → Performance → grave o carregamento com throttling ligado. A linha "Layout Shift" marca cada salto, e clicando nela o navegador mostra qual elemento se mexeu.

Consertar o INP

NovoJS

INP ruim quer dizer uma coisa só: tem JavaScript demais ocupando a única linha de execução que o navegador tem. Enquanto uma função roda, nada responde.

js
// 1. scripts de terceiro NUNCA bloqueando
<script src="analytics.js" defer></script>

// 2. evento que dispara muito precisa de freio
window.addEventListener('scroll', debounce(minhaFuncao, 100), { passive: true });

// 3. trabalho pesado sai do caminho da interação
btn.addEventListener('click', () => {
  mostrarResultado();                    // o que o usuário precisa ver AGORA
  requestIdleCallback(() => registrar()); // o resto espera a folga
});

// 4. animar só transform e opacity — o resto força recálculo de layout
O maior vilão em site de cliente costuma ser terceiro: pixel do Meta, chat de atendimento, mapa do Google embutido, widget de avaliação. Cada um traz o próprio JavaScript. Meça antes e depois de adicionar cada um — e leve o número para a conversa quando o cliente pedir o quinto widget.

Fontes — o gargalo que ninguém vê

NovoCSS

Fonte externa é o segundo maior peso depois de imagem, e afeta LCP e CLS ao mesmo tempo. Três decisões resolvem.

Menos pesos. Duas famílias, dois ou três pesos cada. Seis pesos de duas famílias são uns 400 KB por nada — o olho não distingue 300 de 400 no corpo de texto.

Sempre font-display: swap. Sem ele, o navegador esconde o texto por até três segundos esperando a fonte chegar. O visitante olha uma tela em branco enquanto o texto existe e está pronto.

Considere hospedar você mesmo. Baixar o .woff2 e servir do próprio domínio elimina uma conexão externa, e ainda ajuda na conversa de LGPD — o Google Fonts registra o IP de quem visita.

css
@font-face {
  font-family: 'Inter';
  src: url('/fontes/inter-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;  /* só latim — corta metade do arquivo */
}
A opção de custo zero: a pilha de fontes do sistema. font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif carrega instantaneamente porque já está no aparelho, e fica nativa em cada sistema. Para site de serviço onde a tipografia não é o argumento, é uma escolha defensável — e o LCP agradece.

Campo e laboratório — dois números diferentes

Novo

Esta distinção evita muita discussão inútil com cliente.

LaboratórioCampo
OndeLighthouse, PageSpeed (seção de cima)PageSpeed (seção "Descubra a experiência dos usuários reais")
O que éUma simulação controlada, agoraMedição real dos visitantes nos últimos 28 dias
Serve paraDiagnosticar e comparar antes/depoisSaber a verdade — é o que o Google usa
LimitaçãoVaria a cada execução; não é o que ninguém sentiuSó existe com tráfego suficiente

Fluxo que funciona: use o Lighthouse durante o desenvolvimento para achar e consertar, e o dado de campo um mês depois da publicação para confirmar que o conserto valeu no mundo real.

medir no próprio site
// registra o LCP real de cada visitante — sem biblioteca
new PerformanceObserver((lista) => {
  const ultimo = lista.getEntries().at(-1);
  console.log('LCP:', Math.round(ultimo.startTime), 'ms', ultimo.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });
O alvo sensato: acima de 90 no Lighthouse mobile e os três Vitals no verde. Perseguir 100 costuma custar horas para ganhar meio segundo que ninguém percebe — tempo que renderia mais aplicado na oferta da página.
PARTE 4

Formulários que funcionam no Brasil

Você entrega páginas com CTA de WhatsApp, e está certo — mas uma hora aparece o cliente que precisa de formulário. Quando aparecer, os problemas são sempre os mesmos: máscara de telefone, CEP, spam e mensagem de erro que ninguém entende.

Quando formulário perde para o WhatsApp

Novo
SituaçãoMelhor caminhoPor quê
Serviço local, ticket médio, decisão rápidaWhatsAppConversa começa na hora; formulário vira e-mail que o cliente lê amanhã
Precisa de anexo (planta, exame, documento)WhatsAppMandar arquivo é trivial ali e chato num formulário
Cliente quer os dados organizados numa planilhaFormulárioConversa de WhatsApp não vira relatório
Muitos contatos por dia, com triagemFormulárioCampos obrigatórios filtram quem não é público
Fora do horário comercialOs doisWhatsApp com resposta automática, formulário como alternativa
O erro comum é oferecer os dois com o mesmo destaque. Escolha um caminho principal e deixe o outro discreto. Duas chamadas de igual peso fazem a pessoa hesitar — e hesitação, numa página de venda, é abandono.

O que o navegador já faz de graça

RevisãoHTML

Muita gente escreve cem linhas de JavaScript para validar o que o HTML valida sozinho — e ainda perde o teclado certo no celular, que é o detalhe que mais muda a experiência.

html
<label for="nome">Nome</label>
<input id="nome" name="nome" type="text" required
       autocomplete="name" minlength="3">

<label for="fone">WhatsApp</label>
<input id="fone" name="fone" type="tel" required
       autocomplete="tel" inputmode="numeric"
       placeholder="(67) 99999-9999">

<label for="email">E-mail</label>
<input id="email" name="email" type="email"
       autocomplete="email" inputmode="email">

<label for="msg">O que você precisa?</label>
<textarea id="msg" name="msg" rows="4" maxlength="500"></textarea>
AtributoO que ganha
type="tel" + inputmode="numeric"Teclado numérico no celular — sozinho já reduz abandono
autocompleteO navegador preenche com os dados salvos da pessoa
requiredValidação e aviso nativos, já acessíveis
<label for>Clicar no rótulo foca o campo; leitor de tela anuncia junto
autocomplete="off" em formulário de contato é maltrato: você está impedindo a pessoa de preencher em dois toques. Só faz sentido em campo que não deve ser lembrado, como um código de uso único.

Máscara de telefone e CPF

NovoJS

Sem biblioteca, sem dependência. A ideia é sempre a mesma: tira tudo que não é dígito, limita o tamanho, e reinsere os separadores.

js
function mascaraTelefone(v) {
  v = v.replace(/\D/g, '').slice(0, 11);
  if (v.length <= 10) return v.replace(/(\d{0,2})(\d{0,4})(\d{0,4})/,
      (_, a, b, c) => (a ? '(' + a : '') + (b ? ') ' + b : '') + (c ? '-' + c : ''));
  return v.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3');
}

function mascaraCPF(v) {
  return v.replace(/\D/g, '').slice(0, 11)
          .replace(/(\d{3})(\d)/, '$1.$2')
          .replace(/(\d{3})(\d)/, '$1.$2')
          .replace(/(\d{3})(\d{1,2})$/, '$1-$2');
}

document.querySelector('#fone').addEventListener('input', (e) => {
  e.target.value = mascaraTelefone(e.target.value);
});
Validar CPF é diferente de mascarar. O CPF tem dígito verificador; 111.111.111-11 passa na máscara e não existe. Só valide de verdade se o negócio precisa — para formulário de contato de arquiteta, pedir CPF já é pedir demais, e cada campo a mais derruba a conversão.

CEP que preenche o endereço

NovoJS

O ViaCEP é gratuito, não exige cadastro nem chave, e economiza quatro campos de digitação no celular.

js
const cep = document.querySelector('#cep');

cep.addEventListener('blur', async () => {
  const valor = cep.value.replace(/\D/g, '');
  if (valor.length !== 8) return;

  try {
    const r = await fetch(`https://viacep.com.br/ws/${valor}/json/`);
    const d = await r.json();
    if (d.erro) { avisar('CEP não encontrado'); return; }

    document.querySelector('#rua').value    = d.logradouro;
    document.querySelector('#bairro').value = d.bairro;
    document.querySelector('#cidade').value = d.localidade;
    document.querySelector('#uf').value     = d.uf;
    document.querySelector('#numero').focus();   // manda o foco para o que falta
  } catch {
    avisar('Não consegui buscar o CEP. Preencha o endereço manualmente.');
  }
});
Duas coisas que separam o bom do ruim aqui: sempre deixe os campos editáveis (CEP de rua nova vem errado ou vazio) e sempre tenha o caminho manual quando a consulta falhar. API de terceiro cai, e o formulário não pode cair junto.

Spam — honeypot antes de CAPTCHA

NovoHTML

CAPTCHA é o último recurso, não o primeiro: ele cobra esforço de todo visitante legítimo para barrar robô. Comece pelo que é invisível.

html + js
<!-- campo isca: pessoa não vê, robô preenche -->
<div aria-hidden="true" style="position:absolute;left:-9999px">
  <label>Não preencha este campo
    <input type="text" name="website" tabindex="-1" autocomplete="off">
  </label>
</div>

<script>
const inicio = Date.now();

form.addEventListener('submit', (e) => {
  // 1. a isca foi preenchida? é robô
  if (form.website.value) { e.preventDefault(); return; }
  // 2. preencheu tudo em menos de 3 segundos? é robô
  if (Date.now() - inicio < 3000) { e.preventDefault(); return; }
});
</script>
CamadaCusto para o visitanteBarra
HoneypotZeroA maioria dos robôs simples
Tempo mínimoZeroPreenchimento automatizado
Limite por IP no servidorZeroEnvio em massa
Cloudflare TurnstileQuase zero — invisível na maioria das vezesRobô sofisticado
reCAPTCHA com imagensAlto — e é um problema de acessibilidadeQuase tudo, mas afasta gente
Em formulário de serviço local, as duas primeiras camadas resolvem o problema inteiro. Se ainda vier spam, Turnstile antes de reCAPTCHA — o da Cloudflare é gratuito e quase sempre não pede nada ao visitante.

Erro acessível e envio de verdade

NovoJS

Mensagem de erro em vermelho ao lado do campo não serve para quem não enxerga a cor nem para quem usa leitor de tela. Três atributos resolvem.

html + js
<label for="fone">WhatsApp</label>
<input id="fone" name="fone" type="tel" required
       aria-describedby="erro-fone" aria-invalid="false">
<p id="erro-fone" class="erro" role="alert"></p>

<script>
function marcarErro(campo, mensagem) {
  const alvo = document.querySelector('#erro-' + campo.name);
  alvo.textContent = mensagem;                    // role="alert" faz anunciar
  campo.setAttribute('aria-invalid', 'true');
}
</script>

E para enviar de verdade sem back-end, três caminhos:

CaminhoQuando usa
Formspree / Web3FormsCola a URL no action e o e-mail chega. Plano gratuito cobre site de serviço.
Função serverless (Vercel)Quando precisa validar no servidor, gravar em planilha ou integrar com CRM
Montar link de WhatsAppO "formulário" só organiza a mensagem e abre a conversa já escrita
o terceiro caminho, na prática
form.addEventListener('submit', (e) => {
  e.preventDefault();
  const texto = `Olá! Sou ${form.nome.value}.
Preciso de: ${form.servico.value}
Bairro: ${form.bairro.value}`;
  location.href = 'https://wa.me/5567999999999?text=' + encodeURIComponent(texto);
});
Esse último é o melhor dos dois mundos para o seu tipo de cliente: a triagem organizada do formulário, com a conversa começando na hora — e sem back-end, sem e-mail que ninguém lê, sem custo mensal.
PARTE 5

Testar antes de entregar

A diferença entre o freelancer que some depois da entrega e o profissional que o cliente indica é, na prática, uma lista de verificação. Vinte minutos aqui evitam a mensagem de domingo à noite dizendo que o site está quebrado.

A checklist de entrega

Novo

Rode na URL de produção, não no arquivo local — metade dos problemas só aparece no domínio real.

ItemComo confere
Todo CTA abre o WhatsApp certoClique em todos. Número errado é o erro mais caro e mais comum.
Mensagem pré-preenchida faz sentidoLeia o que chega. Acento quebrado denuncia falta de encodeURIComponent.
Nenhum texto de exemplo sobrougrep -ri "lorem\|exemplo\|xxx\|99999" *.html
Título e descrição de cada páginaÚnicos, com a cidade no título
Miniatura de compartilhamentoCole o link no seu WhatsApp e veja
Favicon aparece na abaRecarregue forçado
Formulário envia e chegaEnvie um de teste e confirme no destino
Página de erro 404Acesse /pagina-que-nao-existe
HTTPS com cadeado, sem avisoTeste também sem www
Lighthouse mobile acima de 90Em aba anônima
Tab chega em tudoTeclado, do topo ao rodapé
Analytics registrandoGA4 → Tempo real, com você na página
Sitemap e robots respondendo/sitemap.xml e /robots.txt
Console limpoNenhum erro vermelho
Transforme isso num arquivo e guarde junto com o projeto. Checklist de cabeça é checklist que encolhe conforme o prazo aperta — e a primeira coisa que some é justamente a verificação do número do WhatsApp.

Quais navegadores importam aqui

Novo

No Brasil, a realidade de tráfego de site de serviço local é mais concentrada do que parece: Chrome em Android domina, Safari no iPhone vem em segundo com folga, e o resto é ruído. Mas o segundo lugar é justamente o que costuma quebrar.

NavegadorTestar?Observação
Chrome AndroidSempreÉ onde está a maioria
Safari iOSSempreOnde aparece diferença. Todo navegador no iPhone usa o motor do Safari — "Chrome no iPhone" é Safari por dentro.
Chrome desktopSempreÉ onde você desenvolve
Safari macOSSe tiver acessoCostuma seguir o do iOS
FirefoxRápidoRaramente diverge hoje
EdgeNãoÉ Chrome por dentro
Internet ExplorerNãoDescontinuado. Se o cliente insistir, é conversa de escopo e de preço.
Antes de usar algo novo: consulte caniuse.com. Ele mostra o suporte por navegador e — mais útil — a porcentagem de usuários no Brasil. É a diferença entre "posso usar?" e "posso usar para este público?".

As manias do Safari no iPhone

NovoCSS

A lista curta do que quebra só lá, e o conserto de cada um.

css
/* 1. 100vh não é a altura visível — a barra do navegador come um pedaço */
.hero { min-height: 100vh; }      /* corta conteúdo no iPhone */
.hero { min-height: 100svh; }     /* certo: small viewport height */

/* 2. campo com fonte menor que 16px faz a página dar zoom sozinha ao focar */
input, select, textarea { font-size: 16px; }

/* 3. rolagem com inércia dentro de caixa */
.rolavel { -webkit-overflow-scrolling: touch; }

/* 4. área segura do notch e da barra inferior */
.rodape-fixo { padding-bottom: calc(12px + env(safe-area-inset-bottom)); }

/* 5. Safari antigo ignora gap em flex — teste antes de depender */
/* 6. position: sticky quebra se algum pai tem overflow: hidden */
O do zoom é o mais traiçoeiro: o visitante toca no campo de telefone, a página dá um zoom que ele não pediu, ele não consegue mais ver o botão de enviar, e desiste. Você nunca vai ver isso acontecer no Chrome do seu computador. Uma linha de CSS resolve — e ela precisa estar lá desde o começo.

Caçar link, imagem e metadado quebrado

NovoJS
console — cole na página publicada
// imagens que não carregaram
$$('img').filter(i => !i.complete || i.naturalWidth === 0)

// links vazios, quebrados ou ainda em "#"
$$('a').filter(a => !a.getAttribute('href') || a.getAttribute('href') === '#')

// links externos sem rel de segurança
$$('a[target="_blank"]').filter(a => !a.rel.includes('noopener'))

// metadados essenciais
({
  titulo: document.title,
  descricao: document.querySelector('meta[name=description]')?.content,
  og: document.querySelector('meta[property="og:image"]')?.content,
  h1: $$('h1').map(h => h.textContent.trim()),
  semAlt: $$('img').filter(i => !i.hasAttribute('alt')).length
})

E do lado do terminal, antes mesmo de publicar:

terminal
grep -ri "lorem\|TODO\|FIXME\|localhost" *.html      # sobras de desenvolvimento
grep -o 'href="[^"]*\.html"' *.html | sort -u        # todos os links internos
grep -o 'src="[^"]*"' *.html | sort -u               # todos os arquivos referenciados

Teste de fumaça automatizado

NovoJS

"Fumaça" vem da eletrônica: liga e vê se sai fumaça. Não testa tudo — testa se o essencial está de pé. Um arquivo, e você roda antes de toda entrega.

teste.mjs — node teste.mjs
import { chromium } from 'playwright';

const SITE = 'https://site-do-cliente.com.br';
const b = await chromium.launch();
const p = await (await b.newContext({ viewport: { width: 390, height: 800 } })).newPage();

const erros = [];
p.on('pageerror', e => erros.push('JS: ' + e.message));
p.on('response', r => { if (r.status() >= 400) erros.push(r.status() + ' ' + r.url()); });

await p.goto(SITE, { waitUntil: 'networkidle' });

const r = await p.evaluate(() => ({
  titulo:   document.title,
  h1:       document.querySelectorAll('h1').length,
  zap:      [...document.querySelectorAll('a[href*="wa.me"]')].length,
  semAlt:   [...document.querySelectorAll('img:not([alt])')].length,
  overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
}));

if (r.h1 !== 1)        erros.push('deveria ter exatamente 1 h1, tem ' + r.h1);
if (r.zap === 0)       erros.push('nenhum link de WhatsApp na página');
if (r.semAlt > 0)      erros.push(r.semAlt + ' imagens sem alt');
if (r.overflow > 0)    erros.push('rolagem horizontal a 390px');

console.log(r);
console.log(erros.length ? erros : 'tudo certo');
await b.close();
É exatamente o que eu rodei a cada mudança neste site — foi assim que apareceu a página que tinha ficado sem o link novo no menu. Copie, troque a URL e as regras pelo que importa no projeto do cliente, e guarde junto com o código.

Como apresentar para o cliente aprovar

Novo

Mandar o link e escrever "pronto, dá uma olhada" é o caminho mais curto para dez mensagens de ajuste desconexas ao longo de três semanas.

Mande a URL de pré-visualização, não a de produção. Com o repositório ligado à Vercel, um branch já vira um endereço próprio — o cliente vê sem que nada esteja no ar de verdade.

Diga o que você quer que ele olhe. "Confere por favor: o telefone, os três serviços, e se o texto da hero está do jeito que você fala com seu cliente. O restante ainda vou ajustar." Sem isso, ele comenta a cor do botão e não percebe que o telefone está errado.

Peça para abrir no celular dele. É onde o cliente dele vai ver, e é onde aparece o que você não testou.

Combine quantas rodadas de ajuste estão incluídas. Duas é o padrão razoável. Isso é conversa de contrato — e está na trilha Negócio.

E registre a aprovação por escrito, mesmo que seja uma mensagem de WhatsApp dizendo "pode publicar". É o que separa "ajuste combinado" de "trabalho novo" três semanas depois.
PARTE 6

Laboratório: ver a diferença

Tudo o que as cinco partes anteriores explicaram em texto, acontecendo aqui do lado. Mexa nos controles — a maioria destes problemas só fica óbvia quando você vê o certo e o errado na mesma tela.

Contraste: o número, não o achismo

NovoCSS

Arraste e veja a razão de contraste mudar. O ponto de virada do WCAG AA para texto normal é 4.5:1 — abaixo disso reprova, e o que parece “cinza elegante” na sua tela vira ilegível no celular ao sol.

cor do texto — —

Projetos de interiores e reforma de apartamento em Campo Grande. Atendimento do projeto à entrega da obra, com acompanhamento semanal.

O momento interessante é perto de 4.5: a diferença visual entre passar e reprovar é pequena, e a diferença para quem tem baixa visão é enorme. É por isso que se mede em vez de olhar.

A cor que some para 8% dos homens

Novo

A paleta de cima usa cor para informar: verde é disponível, vermelho é indisponível. Troque o tipo de visão e veja o que sobra da informação.

simular
disponível
esgotado
últimas vagas
sob consulta
A regra que resolve: a cor nunca pode ser o único portador da informação. Acrescente um ícone, uma palavra ou um padrão. Em deuteranopia, o verde e o vermelho acima viram quase a mesma faixa — e o visitante não tem como saber qual é qual.

Foco visível — e o que acontece sem ele

NovoCSS

Clique em percorrer com o teclado. Os dois lados recebem foco na mesma ordem; só um deixa isso visível.

os dois lados recebem foco — um deles você não vê

Assim não

*:focus { outline: none; }

Assim sim

:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

Quem navega por teclado no lado esquerdo está às cegas: aperta Enter sem saber onde está. E :focus-visible resolve o motivo pelo qual as pessoas apagavam o contorno — ele não aparece para quem clicou com o mouse.

O pulo do layout, acontecendo

NovoCSS

Clique em carregar a imagem e tente clicar no botão verde do lado esquerdo antes de a foto chegar. É exatamente isso que o CLS mede.

tente clicar no botão verde logo depois

Sem width e height

Reforma de apartamento

<img src="obra.webp">

Com espaço reservado

Reforma de apartamento

<img src="obra.webp" width="800" height="600">

O botão do lado esquerdo foge no instante em que a imagem chega. No celular, com o dedo já a caminho, isso é o visitante clicando em outra coisa — e é por isso que o Google trata CLS como sinal de qualidade.

Quanto o peso custa em segundos

Novo

Arraste até o tamanho de uma foto tirada com celular e sem tratamento. O tempo é calculado para 4G brasileiro em condição média — cerca de 1,6 MB por segundo.

peso da imagem — — —
sua foto
bem feita
120 KB
A conversa com o cliente é essa tela. Ninguém discute "otimizar imagem" no abstrato; todo mundo entende "a foto que você mandou faz a página levar dois segundos a mais, e metade das pessoas desiste antes de três".

Máscara ao vivo e erro que o leitor anuncia

NovoJS

Digite só números no campo abaixo. À direita fica o valor cru, que é o que você deve enviar — a máscara é para o olho, não para o servidor.

cru: (vazio)

E o erro de validação, nas duas versões. Clique em tentar enviar:

Só visual

borda vermelha + “Campo inválido”: não diz o que fazer, e o leitor de tela não anuncia

Acessível

role="alert" + aria-invalid + mensagem que ensina o formato

Repare na diferença de texto: “Campo inválido” informa que deu errado. “Informe um WhatsApp com DDD, ex.: (67) 99999-8888” informa o que fazer. A segunda converte; a primeira faz a pessoa fechar a aba.

Zoom de 200% e o que o leitor de tela ouve

Novo

O WCAG exige que a página continue usável com o texto ampliado em 200%. Clique e veja o que acontece com largura fixa:

é o que faz quem não enxerga bem de perto

Largura fixa

Reforma completa em até 90 dias

width: 210px; white-space: nowrap

Largura que cede

Reforma completa em até 90 dias

max-width: 210px — o texto quebra e continua inteiro

E o alternativo de imagem, do jeito que ele realmente chega a quem não vê a tela:

alt="imagem"
alt com palavras-chave empilhadas
alt que descreve
O do meio é o mais comum em site feito às pressas: alguém tentou fazer SEO no alt. O resultado é que a pessoa cega ouve uma lista de palavras soltas — e o Google, que entende a diferença há anos, também não ganha nada com isso.