Bit Health
Reduzi o tempo de carregamento da home de ~60s para 2s em usuários ativos em 3G.
Resumo
Os usuários abriam o app na academia em 3G e esperavam quase um minuto para a home terminar de carregar. A requisição mais lenta arrastava a tela inteira junto. Indexei o join que não tinha índice, movi as seções compartilhadas para trás de um cache, reorganizei o lazy loading, e parei de refazer a home a cada volta. Cheguei a 2 segundos em conexões confiáveis.
Contexto
Bit Health é um app de treino. A home é a tela em que o usuário cai depois do login, e é onde tudo acontece: continue assistindo, recomendações, conteúdos adicionados recentemente, categorias. A maioria dos usuários abre o app na academia ou num parque público. Wifi não é garantia. Toda decisão a partir daí tem que assumir 3G.
O desafio
A home disparava várias requisições em paralelo a cada abertura. Para usuário novo, dava conta. Mas para usuário ativo, alguém com 'continue de onde parou' populado, a requisição mais lenta arrastava a tela inteira junto. Estávamos vendo perto de um minuto até a home carregar de verdade. Os usuários abandonavam antes de a gente ter algo pra mostrar.
O que fizemos
- 01
Indexamos o join que estava nos puxando pra baixo
A consulta de 'continue assistindo' fazia join na tabela de progresso por user_id e content_id. Nenhuma das duas colunas tinha índice. Toda abertura batia no banco com uma query que não tinha como responder rápido. Adicionei o índice que faltava e enxuguei o SELECT para buscar só as colunas que a home realmente renderiza: thumbnail, título, percentual de progresso. Essa única mudança domou a requisição mais lenta.
- 02
Separamos conteúdo compartilhado de conteúdo personalizado
Parte da home é igual para todo usuário num dado momento: recomendações, lançamentos, categorias. Nada disso precisava ser recalculado por requisição. Movi essas seções para trás de um cache com refresh agendado, e deixei só as chamadas personalizadas batendo no banco por usuário.
- 03
Reorganizamos o lazy loading no cliente
O Angular estava bootando módulos que a home não precisava. Reestruturei a árvore de rotas para a home carregar só o que a home renderiza. Configurações, edição de perfil e compras viraram chunks lazy, carregados só quando o usuário ia até lá.
- 04
Paramos de refazer a busca toda vez que voltava
O NgRx estava ingênuo. Toda vez que o usuário voltava para a home, refazíamos tudo do zero. Fresco, sim. Mas lento. Mudei a home para ler do store por padrão. Quando o usuário terminava de assistir um vídeo, eu disparava o refresh em background durante o redirect, então quando ele chegava na home o store já estava atualizado. Para ações que mexiam só em estado local, tipo favoritar um vídeo ou atualizar progresso, eu patcheava o store direto e pulava o round-trip de rede inteiro.
Resultados
15s → 2s
Tempo de carregamento da home
em conexões confiáveis (redução de 87%)
~60s → 2s
Usuários ativos em 3G
resolveu a queixa de quase-um-minuto pra carregar
~3×
Conversão de demo em contrato
reportada pelo co-fundador após demos em eventos da indústria (1 em 10 → 3 em 10)
Em retrospecto
- Mantivemos a arquitetura multi-endpoint porque cada endpoint moldava sua própria resposta: enriquecimento de URL de thumbnail, formatação de categoria. Um endpoint agregador, com moldagem feita num lugar só, teria sido mais rápido e mais simples de raciocinar. Eu pressionaria por essa abordagem mais cedo da próxima vez.
- Todo card da home fazia join na tabela de mídia pra buscar sua URL de thumbnail. Era um N+1 que a gente nunca resolveu. Eloquent tem eager loading exatamente pra esse caso, e se eu tivesse auditado as chamadas do ORM e arrumado o carregamento das relações, acredito que teríamos colocado a home abaixo de 500ms. Não investi o tempo na hora.