Pensa num restaurante que funciona perfeitamente nas tardes de semana. Fila zero, atendimento rápido, prato chega quente.
Aí chega o feriado com promoção e o lugar enche três vezes. A cozinha não mudou. O cardápio não mudou. Mas o cliente espera 40 minutos, desiste, e vai embora.
Checkout de infoproduto funciona exatamente assim.
No organic, o link chega devagar, o celular carrega no tempo dele, o botão aparece, a venda fecha. Aí um influenciador manda o link. Pico instantâneo. O mesmo bundle, mas agora são centenas de pessoas tentando pagar ao mesmo tempo, na 4G, no celular delas. O JavaScript do passo de pagamento concorre com o JavaScript do dashboard, do upsell, do pixel. O primeiro load não aguenta. A sessão morre. O backoffice nem registra a tentativa.
No dia seguinte alguém diz que a campanha foi ruim.
Enquanto isso, no Slack, a squad de Growth e Marketing debatia com a equipe técnica qual ferramenta usar e se valia migrar de tecnologia. Não era a pergunta errada. Só estava na ordem errada.
Antes de mexer em qualquer coisa, eu precisava entender onde exatamente a venda estava morrendo. Não "o site tava lento". Isso não resolve nada. A pergunta era: o comprador chegou até o botão de pagar? Chegou. Clicou? Sim. O pagamento saiu? Não saiu.
Adotei o Sentry como ferramenta de observabilidade, não pra apagar incêndio, mas pra coletar dados. Foram semanas rastreando esse momento específico, o instante entre o clique e a confirmação do pagamento, e cruzando com os horários dos picos de campanha. Só depois que o quadro ficou claro é que fui pro ataque.
O resultado apareceu no painel um mês depois: +10% de conversão.
Esse artigo é sobre esse processo. O que eu vi no Sentry, o que eu mudei, e por que a ordem importou.
Tabela de Conteúdo
- Sintoma: a campanha quebrava na hora de pagar
- Causa: o checkout carregava código demais
- O que mudou: página pronta, código sob demanda, servidor perto
- Como eu medi: quatro perguntas antes do Lighthouse
- Resultado: +10% no mês
- O que copiar no próximo projeto
1. Sintoma: a campanha quebrava na hora de pagar
Checkout de infoproduto no organic é uma fila educada. Na campanha de influenciador não é. O link sai, o pico chega junto, e o passo de pagar ou aguenta o primeiro load ou transborda.
Timeout é o browser não terminar o trabalho a tempo. Pensa num garçom que tem duas mesas pra atender ao mesmo tempo: ele pega o pedido das duas, mas só consegue entregar um prato de cada vez. A pessoa já tinha escolhido o produto, o pixel de view já foi, faltava só o POST de pagamento. Se o JS daquele passo ainda tava baixando o mundo inteiro, a sessão morria antes de chegar na cozinha. O backoffice nem via a tentativa. No dia seguinte alguém falava "a campanha foi ruim".
Transbordo é o irmão do timeout: origem fria, fila, saturação, bundle grande demais pro celular na 4G. Os dois matam a mesma linha do painel.
No canal a resposta fácil era stack. Vue 3, Nuxt, Lighthouse verde. Eu fiz isso também. Nenhum desses, sozinho, prova que vendeu. O Lighthouse roda no lab, no cabo, num CPU que não é o do comprador no horário da live. É como testar a velocidade do restaurante numa terça de manhã com dois clientes.
Se o teu checkout só dói no organic, o bundle inchado passa. Dói quando o influenciador manda o link.
2. Causa: o checkout carregava código demais
O checkout era um micro frontend (MFE). Vários times, várias fatias, um único caminho de pagamento. Isso é bom pra organização de time. Time de cupom não espera time de pixel. MFE resolve deploy. Não resolve o que o celular baixa nos primeiros dois segundos.
O que rolava: cada fatia trazia o que era dela e um pedaço do vizinho. Pensa num entregador que, pra levar a pizza, também carrega a sobremesa, o refrigerante da mesa do lado e o pedido do vizinho de bloco, "só pra não fazer duas viagens". Na prática: runtime duplicado, pixel, upsell, modal, às vezes um resto de área logada. O comprador, no passo de pagar, baixava tudo isso antes de ver o botão.
SPA seca piora. HTML quase vazio, JS inteiro do MFE, parse, hidrata, aí o botão existe. No Wi-Fi do escritório passa. Na live, no celular, é timeout.
A conta que ficou pra mim foi uma frase só: o passo atual baixa o JS do passo atual. O resto espera o clique.
"A gente tem MFE" descreve o organograma. Não descreve o Network daquele passo. Se você abre o DevTools no pagar e ainda vê chunk de dashboard, o MFE não te salvou. Ele te cobrou o first load.
3. O que mudou: página pronta, código sob demanda, servidor perto
Tinha BFF (Backend For Frontend). Por isso SSR (Server-Side Rendering) era opção de verdade, não slide. Sem BFF, o browser monta o passo de pagar com vários round-trips e um bundle que hidrata tudo. Com BFF, o servidor já conhece o passo. É como o garçom que já sabe o seu pedido antes de você sentar. Dá pra mandar HTML que mostra preço, meio de pagamento e botão sem esperar o mundo inteiro hidratar.
Foi isso que eu fiz no first paint do passo crítico. O JS ainda hidrata o que o POST precisa. Não hidrata o dashboard no meio do caminho.
Vue 2 pra Vue 3 e Nuxt 3 entrou aqui. Não porque "a lib nova é mais rápida" no vazio. Porque o legado não entregava SSR e split de verdade no MFE com a mesma facilidade. E eu não virei o checkout inteiro num dia. As rotas que a campanha batia foram pro stack novo. O resto ficou no legado. Roteador de borda decide. Sem big bang.
SSR sozinho não basta. Se a hidratação ainda manda o MFE inteiro, você só adiou o timeout.
Aí entra JavaScript sob demanda. Code splitting é o bundler gerar mais de um arquivo. Lazy load é o browser só pedir o arquivo quando o passo precisa. É a diferença entre um supermercado que bota tudo no carro de uma vez versus um que entrega o que você pediu agora e deixa o resto pra quando você pedir. Os dois andam juntos, mas:
const PayStep = () => import('./PayStep.vue')
Se o PayStep.vue no topo importa upsell, chart e admin, o import() é teatro. O chunk do pagar ainda é o mundo.
O teste não é ter import() no PR. É abrir o Network no passo de pagar e não ver o JS do admin. Order bump, meio extra, modal que quase ninguém abre: depois do clique.
Tem um furo irmão: cada fatia do MFE traz o Vue de novo. Você splittou o app e duplicou o runtime.
Onde isso invertia tudo
SSR na tela interna. Bundle único no checkout. O Lighthouse do admin ficava lindo. A campanha caía. A regra que ficou: SSR quando o first paint é pagar. Lazy quando o código só existe depois do clique.
Depois do split, o arquivo ainda precisa chegar. Eu já tinha cortado o JS e o timeout continuava em algumas regiões. O chunk do PayStep saía longe, ou frio, ou sem compressão. É como ter a pizza pronta na cozinha mas o motoboy do outro lado da cidade.
CDN (Content Delivery Network) perto de quem compra. Cache longo no estático com hash no nome. Gzip no que sobrou. Brotli se o edge já tiver, sem religião. Se o lazy sai de origem fria, você não fez lazy load. Fez "espera a origem acordar". O spinner só mudou de lugar. O POST continua sem sair.
4. Como eu medi: quatro perguntas antes do Lighthouse
Eu parei de tratar ferramenta como tese. Trato como pergunta.
O hábito que ficou: no pico, eu abria o Network no pagar antes do Lighthouse. Se tinha chunk de dashboard, o PR de bundle ainda não tinha acabado.
O Sentry pergunta: o timeout foi no pagar, ou foi pixel quebrado depois da venda? Sem a ação do usuário no evento, os dois viram o mesmo TypeError. Aí você trata ruído como incidente, ou trata pagar que não saiu como "JS genérico". É como um hospital que contabiliza gripe e infarto como "paciente com febre": os números somam, mas a causa é diferente.
O Datadog pergunta: o caminho até o arquivo e até o POST estava saturado? Sentry conta o erro da jornada. Datadog conta se a origem ajoelhou. Timeout com origem ok é bundle. Origem ajoelhada com bundle ok é infra. No pico os dois acontecem. Uma lente só te faz otimizar a errada.
O Lighthouse pergunta: no lab, o que está bloqueando o first paint? Eu coletei. Serve pra hipótese. Não fecha receita. Não tem influenciador. Não tem a fila da CDN.
O backoffice pergunta: vendeu? Sem a métrica de venda, bundle menor é hipótese. Essa pergunta fecha o mês.
Recibo (o que fecha, o que não linko)
PRs e dashboards reais ficam atrás de NDA. O que o leitor consegue checar é este recorte. O +10% é o número do painel. O resto é o hábito + o filtro. Sem print forjado.
| Lente | O que eu olhava | O que este post pode mostrar |
|---|---|---|
| Backoffice | conversão do mês, pico na campanha | +10% (número do cliente) |
| Sentry | timeout no passo de pagar, não TypeError órfão | query abaixo (sintético) |
| Datadog | TTFB (Time To First Byte) / saturação / região no chunk do pagar | o que eu perguntava |
| Lighthouse | first paint no lab | limite: print não arquivado |
| Código | split, SSR, CDN | PRs internos. Não linko |
Network no /pagar. Sintético, de propósito. Sem kB inventado. O furo é a presença do arquivo, não um score:
antes (furo)
PayStep.js
vue.runtime.js
dashboard.chunk.js ← não deveria estar no pagar
admin-charts.js ← idem
depois (o teste)
PayStep.js
vue.runtime.js
Se o dashboard ainda aparece, o import() é teatro. Não importa o Lighthouse.
Sentry. Mesmo recorte do complementar. Cola no Discover:
extra.phase:checkout.pay.submit AND (
error.type:TimeoutError OR message:*timeout* OR mechanism:*timeout*
)
Pixel quebrado depois da venda não entra nesse filtro. É outro phase.
Datadog, a pergunta que eu fazia no pico: o TTFB do chunk do PayStep subiu na região da campanha, ou a origem tava ok e o bundle que era grande? Uma lente só te faz otimizar a errada.
Cupom inválido, cartão recusado, form incompleto: a regra funcionou. Breadcrumb. Não exception. Exception é gateway 500, timeout de rede, SDK que estourou no submit. Se misturar, o board grita e você trata recusa de cartão como queda de conversão.
5. Resultado: +10% no mês
O cliente leu o painel. +10% de conversão no mês. Pico nas campanhas de influenciador.
Esse número não veio de uma ferramenta só. Veio de três camadas juntas:
- Métrica de venda: backoffice do cliente. Quem vende lê isso.
- Jornada: Sentry com timeout/transbordo no pagar, não TypeError órfão.
- Caminho: Datadog com TTFB, saturação, região.
Lighthouse ficou de hipótese de lab. Não fecha o mês.
O +10% não veio de silenciar recusa de cartão. Veio de o JS do pagar chegar a tempo, e de eu mapear o timeout no Sentry no mesmo dia que ele acontecia.
Eu toquei esse recorte sozinho: o caminho do pagar, o split, essas lentes. Depois pesou numa promoção. O que ficou copiável foi a ordem: painel primeiro, lab por último. Bundle menor sem esse fechamento era só PR bonito.
6. O que copiar no próximo projeto
Antes de mergear "otimizar o bundle":
- Qual jornada perde dinheiro se o JS atrasar? (pagar, não o admin)
- No Network do passo de pagar, aparece JS de dashboard?
- SSR no first paint do pagar, ou SPA seca esperando hidratar?
- O
import()puxa o passo, ou o passo importa o mundo no topo? - O chunk lazy está no edge, com hash e gzip, ou sai frio?
- Venda (backoffice) e obs (Sentry/Datadog) olham o mesmo número, ou só o Lighthouse?
- Timeout/transbordo alertam sintoma de usuário, ou qualquer JS?
Ordem: backoffice → Sentry → Datadog → Lighthouse
Se o PR não responde 1 e 6, pode ser um PR bom. Não é este case.
O número que paga é a conversão. Vue 3, MFE e Lighthouse entram se ajudam esse número. Se não ajudam, são logo.
Última campanha que você pegou: olhou primeiro o timeout no Sentry, o TTFB no Datadog, ou o backoffice?
Top comments (0)