Minha landing tinha 386 KB de SPA e comia 2 de cada 3 cliques pagos. Achei hoje de madrugada
Contexto: construi o Dosy, um app Android que organiza a medicacao de uma FAMILIA inteira numa conta so. Nao e mais um lembrete de remedio individual sao varios pacientes (mae idosa, filho em tratamento, voce, e ate o pet), cada um com a lista dele, e o dia de todos numa tela. Alarme nativo em tela cheia que toca no silencioso, historico de adesao, relatorio PDF pro medico, e um segundo cuidador que acompanha e marca dose junto.
Ontem subi 4 anuncios no Meta pra achar testadores. Resultado do dia:
- 8.720 pessoas alcancadas
-
- 458 cliques, CTR de 5,4% (umas 2x a media da categoria)
-
- CPC de R$ 0,21
-
- 218 visualizacoes de pagina
-
- zero cadastros
Gastei um tempo achando que era copy. Nao era.
O que os numeros do GA4 diziam, e que eu demorei pra ler direito: 263 usuarios dispararam page_view e so 86 dispararam o evento proprio da landing. Os dois eventos estao no mesmo arquivo. A diferenca e QUANDO cada um roda: o page_view sai assim que o gtag inicializa, e o outro so quando o componente React da pagina monta.
Ou seja: 177 pessoas carregaram o suficiente pro Analytics existir, e foram embora antes da pagina existir.
Fui medir o que a landing realmente pedia pro navegador antes do primeiro pixel:
index .............. 121,4 KB
react-vendor ....... 101,9 KB
dosy ............... 78,3 KB
supabase ........... 51,0 KB
tanstack ........... 20,2 KB
css ................ 6,5 KB
+ runtime e outros
TOTAL .............. 386,6 KB comprimidos (~1,5 MB descomprimidos), 10 arquivos
Mais um chunk lazy da propria pagina. Mais um @import de Google Fonts DENTRO do CSS que e a pior forma possivel de carregar fonte, porque o navegador so descobre o pedido depois de baixar o CSS e ai serializa mais duas idas ao servidor.
E o <body> do HTML servido era literalmente isso:
<body><div id="root"></div></body>
2 KB de HTML sem uma palavra dentro.
O erro de raiz nao foi performance. Foi arquitetural, e obvio depois: eu tratei uma pagina de captura como se fosse uma rota da aplicacao. Ela morava dentro do SPA porque era comodo reaproveitar componente e estilo. So que trafego pago do Feed do Facebook abre no WEBVIEW do app, num Android de entrada, em 4G. Nesse ambiente, 386 KB e varios segundos de tela branca e ninguem espera tela branca de um link de anuncio.
O tempo de engajamento somado confirmava, e eu tinha esse dado na mao o tempo todo:
/quemdeu 48 sessoes -> 70 segundos NO TOTAL
/juntos 32 sessoes -> 0 segundos
/lembrete 31 sessoes -> 1 segundo
/descanso 23 sessoes -> 0 segundos
Refiz as landings como HTML estatico gerado do mesmo arquivo de conteudo que alimentava as versoes React (uma fonte da verdade so). CSS inline, fonte do sistema, e JavaScript apenas pra medir e enviar o formulario nunca pra desenhar.
386,6 KB -> 6,0 KB na rede. TTFB de ~240 ms.
Tres coisas que aprendi e que valem mais que o numero:
-
Pagina que recebe trafego pago nao e rota de aplicacao, e documento. Se o conteudo depende de download pra existir, voce esta pagando por clique de gente que nunca vai ver o conteudo.
-
Instrumente dois eventos em momentos diferentes do boot. Foi so o vao entre
page_viewe o evento do componente que revelou o problema. Um evento so teria mostrado "chegou pouca gente" e eu ia culpar o anuncio pelo resto da semana. -
Cuidado com o que o build assume do ambiente. Na primeira geracao das paginas estaticas, o formulario saiu apontando pro Supabase de LOOPBACK, porque o
.env.localda minha maquina aponta oVITE_SUPABASE_URLpro banco local donpm run dev. Ia subir bonita, aceitar o e-mail e o POST morreria em 127.0.0.1 no celular de quem clicou. Zero erro no build, zero cadastro, zero pista. Coloquei um guard que quebra o build se a URL escolhida for loopback.
No mesmo dia achei tambem um honeypot que devolvia {ok:true} e ia embora sem gravar nada. Pra bot, perfeito. Pra uma pessoa cujo gerenciador de senha preencheu o campo escondido, a tela dizia "Pronto" e o cadastro simplesmente nao existia. Agora grava marcado, e so o e-mail e pulado.
A parte do pedido, e vou ser direto porque acho que voces preferem assim: o app esta em teste fechado no Google Play e eu preciso de 12 pessoas usando por 14 dias e a exigencia e de 14 dias CONTINUOS, entao quem sai no meio rebobina o relogio.
Se voce cuida do remedio de alguem em casa (pai, mae, filho, ou voce mesmo com contnuo), e o publico exato pra quem isso foi feito, e eu quero MUITO a reclamacao de voces inclusive "essa tela ta feia" e "nao entendi esse botao".
- Grupo: https://groups.google.com/g/dosy-testers
-
- Depois o opt-in: https://play.google.com/apps/testing/com.dosyapp.dosy
-
- Entre com a MESMA conta Google da Play Store do celular, senao o app nao aparece
Stack, pra quem tiver curiosidade: React 19 + Vite + Capacitor, Supabase atras, e o alarme em Java nativo porque WebView nao da conta de alarme confiavel em Doze mode.
Critica de arquitetura e de UX e bem-vinda. Se tiver furo, prefiro descobrir agora.