Melhoria de desempenho
- Antes: ~1,8 segundo
- Depois: <10 milissegundos
- Melhoria: ~100x+
Medido com PayCal Lens.
Para administradores de empresas, a página Members agora parece praticamente instantânea, mesmo em organizações maiores.
Resumo executivo
| Página afetada | Business Members — a grade que lista cada membro de uma empresa com colunas financeiras calculadas |
| Antes | ~1,8 segundo de tempo de servidor por carregamento |
| Depois | Milissegundos de um dígito em cache hits (~100x+); cache misses também são mais rápidos que o caminho antigo |
| Causas raiz | Um padrão de consultas N+1 contra Redis e recomputação completa de um ano de cálculos de payroll em cada visualização |
| Correção | Lookups Redis em batch/pipeline, mais cache materializado da grade final com invalidação explícita |
| Frescor dos dados | A cache é invalidada imediatamente em qualquer mudança relacionada a membros; expiração de 5 minutos limita staleness como rede de segurança |
Por que estamos publicando isto
Desempenho faz parte da transparência. Quando uma página é lenta, usuários merecem saber se a lentidão é inerente ao trabalho ou resultado de uma falha de desenho evitável. Neste caso era a segunda opção, duas vezes, e os detalhes valem ser compartilhados porque ambos os defeitos são erros comuns em software web.
Nada neste artigo envolve problema de segurança ou exposição de dados de usuários. É apenas uma história de engenharia sobre tornar rápida uma página lenta.
O que a página faz
A página Business Members lista cada membro de uma empresa. Ao lado de nome e função, a grade mostra cinco colunas financeiras calculadas:
Nenhum desses valores é armazenado como número final. Eles são calculados a partir das work entries brutas de cada membro, os registros de turno armazenados no Redis. Produzir uma linha significa carregar perfil, entradas do ano inteiro, separar horas regulares e extras, e somar pagamento bruto.
- Bruto acumulado no ano — ganhos totais até agora no ano
- Total de horas — todas as horas trabalhadas este ano
- Horas regulares — horas na taxa padrão
- Horas extras — horas acima do limite regular
- Trailing baseline — referência móvel usada para comparação
O problema, parte 1 — O padrão de consulta N+1
A implementação original fazia uma consulta para buscar a lista de membros e depois, para cerca de 100 membros, fazia round-trips Redis separados e sequenciais: um para perfil e outros para o ano completo de work entries.
Empresas pequenas quase não eram afetadas; a latência empilhada aparece quando organizações crescem para dezenas ou centenas de membros.
Esse é o padrão clássico “N+1”: uma consulta para a lista, depois N consultas uma por vez. Cada lookup Redis é rápido, mas cada round-trip paga latência fixa de rede; em sequência, esses custos se acumulam.
O banco nunca foi o gargalo. A conversa com o banco era.
O problema, parte 2 — Recalcular tudo em cada visualização
O segundo defeito ampliava o primeiro. Toda a matemática de payroll, separar entradas do ano em horas regulares e extras, calcular bruto acumulado e baseline, para cada membro, era refeita do zero em cada visualização.
Work entries não mudam com frequência. Entre duas visitas consecutivas, os dados quase sempre são idênticos. Mesmo assim, cada visita pagava o custo completo de recalcular resultados recém-calculados e descartados.
Combinados, os dois defeitos produziam cerca de 1,8 segundo de tempo de servidor por carregamento, medidos com PayCal Lens.
A correção, parte 1 — Redis pipelining
Redis oferece pipelining: enviar um lote de comandos em um único round-trip e receber todas as respostas juntas. O servidor agora faz todas as perguntas de uma vez.
Adicionamos Database::pipelineHgetall() e convertemos a grade de membros. Perfis são carregados em um round-trip e work entries em outro.
// Before - one round-trip per member, latency stacks linearly
foreach ($memberIds as $id) {
$profiles[$id] = Database::hgetall($profileKey($id));
}
// After - one round-trip for the whole batch
$profiles = Database::pipelineHgetall(array_map($profileKey, $memberIds));
Essa mudança sozinha reduziu centenas de pagamentos sequenciais de latência para poucos.
A correção, parte 2 — Cache materializado
Pipelining torna mais barato alimentar o cálculo; a segunda correção evita repetir o cálculo. Introduzimos BusinessMembersCache, cache server-side que armazena no Redis a grade final calculada.
Duas regras governam o frescor da cache:
- Expiração de 5 minutos. Cada grade em cache expira automaticamente após 5 minutos.
- Invalidação explícita em mudança. Qualquer mudança relacionada a membros remove a grade em cache imediatamente.
Identidade de membro e verificações de permissão deliberadamente não são cacheadas. Cada request ainda executa todo o caminho de access control.
Impacto
- Cache hits servem a grade em milissegundos de um dígito — melhoria ~100x+ sobre os ~1,8 segundo anteriores.
- Cache misses continuam mais rápidos que a página antiga, porque a recomputação agora usa lookups em batch com pipeline.
- A correção é testada. Testes contract e unit cobrem o comportamento de invalidação da cache.
What We Took Away From This
- Measure before optimizing. PayCal Lens told us exactly where the 1.8 seconds went. Without per-request timing instrumentation, both flaws would have been guesses.
- Latency stacks; batch it. Many small fast queries issued sequentially are slower than one large batched query. Round-trips, not data volume, dominated this page.
- Cache finished work, invalidate eagerly. A cache is only trustworthy if every write path that affects it also clears it. The expiry is a safety net, not the mechanism.
Continuaremos publicando relatos de engenharia como este no Centro de Transparência.
Fatos de engenharia
- Commit(s):
3db2229b,2b3eafb8,f63773ea - Files Changed: 14 (under
html/) - Tests Added: 12 (
BusinessMembersCacheTest— 9 cases;LensRenderTest— 3 cases) - Tests Passing: 1901 (full PHPUnit suite, June 2026)
- Performance Impact: ~1.8 s → ~7 ms (<10 ms on cache hits)
- Production Status: Deployed