DEV Community

Cover image for +10% de conversão no checkout no mês. A campanha quebrava na hora de pagar
Tiago Vilas Boas (Montanha)
Tiago Vilas Boas (Montanha)

Posted on

+10% de conversão no checkout no mês. A campanha quebrava na hora de pagar

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

  1. Sintoma: a campanha quebrava na hora de pagar
  2. Causa: o checkout carregava código demais
  3. O que mudou: página pronta, código sob demanda, servidor perto
  4. Como eu medi: quatro perguntas antes do Lighthouse
  5. Resultado: +10% no mês
  6. 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')
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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*
)
Enter fullscreen mode Exit fullscreen mode

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":

  1. Qual jornada perde dinheiro se o JS atrasar? (pagar, não o admin)
  2. No Network do passo de pagar, aparece JS de dashboard?
  3. SSR no first paint do pagar, ou SPA seca esperando hidratar?
  4. O import() puxa o passo, ou o passo importa o mundo no topo?
  5. O chunk lazy está no edge, com hash e gzip, ou sai frio?
  6. Venda (backoffice) e obs (Sentry/Datadog) olham o mesmo número, ou só o Lighthouse?
  7. 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)