DEV Community

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

Pessoal, tinha campanha de influenciador no ar. Eu abria o Sentry e via timeout. O canal falava de Lighthouse.

Checkout de infoproduto, alto volume, micro frontend. O link saía, o pico chegava junto, e o passo de pagar transbordava. A sessão morria. No dia seguinte o painel caía e a conversa era score. Ou Nuxt.

Aí o backoffice do cliente fechou o mês. +10% de conversão. Pico nessas campanhas. Métrica de venda, cruzada com o que eu via no Sentry (jornada de pagar) e no Datadog (o caminho até o arquivo). O JS chegou a tempo. O timeout apareceu no mesmo dia.

O ponto é: o Lighthouse verde não pagou. O POST de pagar, sim.

Um limite só: não tenho o print do Lighthouse daquela janela no arquivo público. O +10% veio do painel.

Como etiquetar o Sentry pra isso não virar TypeError órfão fica no complementar. Aqui é o dinheiro.

Tabela de Conteúdo

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 first load ou transborda.

Timeout, pra mim, era o browser não terminar o trabalho a tempo. A pessoa já tinha escolhido o produto. Muitas vezes o pixel de view já tinha ido. Faltava o POST de pagamento. Se o JS daquele passo ainda tava puxando o mundo, a sessão morria. O backoffice nem via a tentativa. No dia seguinte alguém falava “a campanha foi ruim”.

Transbordo é o irmão: 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.

Se o teu checkout só dói no organic, o bundle inchado passa. Dói quando o influenciador manda o link.

2. Causa: o MFE baixava o time inteiro

O checkout era um micro frontend. Vários times, várias fatias, um único caminho de pagamento. Isso é bom pra merge. 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: o host puxava o shell e, se ninguém olhasse o grafo, cada fatia trazia o que era dela e um pedaço do vizinho. Runtime duplicado. Pixel. Upsell. Modal. Às vezes um resto de área logada. O comprador, no passo de pagar, baixava 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.

Pra mim a conta virou 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 faturou o first load.

3. O que mudou: SSR, JS sob demanda, CDN

Tinha BFF. Por isso SSR era opção de verdade, não slide. Sem BFF, o browser monta o pagar com vários round-trips e um bundle que hidrata tudo. Com BFF, o servidor já conhece o passo. Dá pra mandar HTML que serve pra pessoa ver preço, meio e botão sem esperar o mundo hidratar.

Foi isso que eu fiz no first paint do 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 / 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. Os dois andam juntos. Split sem lazy: você ainda baixa os chunks “por precaução”. Lazy sem split de verdade:

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 região. O chunk do PayStep saía longe, ou frio, ou sem compressão.

CDN 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 lentes

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”. O contrato disso está no outro texto.

O Datadog pergunta: o caminho até o arquivo e até o POST tava 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. A resposta está no próximo bloco.

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 / 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 ver o timeout no mesmo dia.

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 PR

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

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)