perf(racetrack): pista fora do layout — scaleX/translateX no lugar de width/left (#59, fatia 2) - #113
Conversation
…left (#59) Fatia 2 da #59. Cada jogador na pista animava DUAS propriedades de layout (`width` na barra e `left` no marcador), com spring retargetado a até 8,3 Hz (PROGRESS_THROTTLE_MS = 120). Com ABSOLUTE_MAX_PLAYERS = 30, o pior caso são 60 springs disparando layout + paint na mesma main thread da textarea — a área sagrada do produto. A barra passa a ocupar o trilho inteiro e encolher por `scaleX` com `transform-origin: left`; o marcador passa a usar `translateX`, com um wrapper da largura do trilho para que `x: "<pct>%"` equivalha ao antigo `left: <pct>%` e o -8px do `calc()` vire margem negativa do ícone. Só transform — nada de layout. Geometria medida idêntica (barra e marcador) em 0/25/50/75/100%. Refs #59
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
🩺 PR Doctor · parecer · 2026-07-30Veredito: APROVADA — merge. Classificação confirmada §7.3 normal (só Base: Gate reproduzido localmente (worktree O que eu verifiquei por conta própriaA ressalva de método do corpo é honesta e está certa (§11 da
Um ponto que o corpo não trata e eu fui conferir: o wrapper agora é full-width e translada, Acceptance criteria do 🗂️ Curador (28/07)
Fatia 2 é a última do escopo e os itens adiados saíram de propósito (coalescing e Sobre a ressalva visual declaradaAceito, e ela é ainda menor do que o corpo assume: o 📌 Achado fora de escopo — confirmado, fica para o 🗂️ Curador: |
Contexto
Fatia 2 (e última) da #59, no escopo que o 🗂️ Curador fixou no comentário de 28/07 —
que por sua vez veio do item 9 do 🏛️ Parecer do Conselho. A fatia 1 (#105) cortou os
re-renders de React; sobrou o custo que nenhum
memoalcança: a pista animavaduas propriedades de layout por jogador.
RaceTrack.tsx:144-146—initial={{ width: 0 }}/animate={{ width: "<pct>%" }}RaceTrack.tsx:153-156—animate={{ left: "calc(<pct>% - 8px)" }}Ambas
spring, retargetadas a até 8,3 Hz por jogador (PROGRESS_THROTTLE_MS = 120).Com
ABSOLUTE_MAX_PLAYERS = 30(src/lib/room.ts), o pior caso são 60 springs delayout + paint na mesma main thread da
textarea— exatamente a área sagrada.O que mudou (1 arquivo, 14 linhas)
w-full) e encolhe porscaleXcomorigin-left. O gradiente vive no espaço local do elemento, então acompanha a escala —o degradê visível continua indo de
<cor>40até<cor>.translateX. Um wrapper da largura do trilho fazx: "<pct>%"(percentual da própria largura) equivaler ao antigo
left: <pct>%; o-8pxdocalc()virou a margem negativa (-ml-2) do ícone. O wrapper épointer-events-none.Nada mais foi tocado:
PROGRESS_THROTTLE_MS,broadcastProgress,sanitizeResultse ocaminho do
finishestão intactos, como o Parecer e o escopo do Curador exigem.Evidência
1. Geometria idêntica (AC 2). Medição de
getBoundingClientRect()no navegador,marcação antiga × nova lado a lado, trilho de 400px:
w=0 @0→w=0 @0x=-8→x=-8w=100 @0→w=100 @0x=92→x=92w=200 @0→w=200 @0x=192→x=192w=300 @0→w=300 @0x=292→x=292w=400 @0→w=400 @0x=392→x=3922. Custo na main thread. 30 jogadores × 300 atualizações (18.000 alvos), forçando o
flush de estilo+layout a cada iteração — mediana de 3 execuções:
width+leftscaleX+translateX−49,8% de tempo de main thread por rodada de atualização.
Método, honestamente: as duas medições são num harness sintético de DOM montado no
navegador (mesma marcação e mesmo CSS, sem React e sem framer-motion), não no app rodando.
Motivo: no Browser pane headless o rAF congela e os springs do framer-motion não avançam —
um "antes/depois" visual ali provaria nada (a barra ficaria parada no
initial). O quesustenta a ponte até o componente real é o passo 3.
3. O framer-motion emite mesmo o transform esperado (era o ponto mais arriscado do diff):
em
framer-motion@11.18.2,getValueAsTypesó anexapxquando o valor énumber— stringpassa verbatim, então
x: "50%"viratranslateX(50%), e é por isso que o wrapper precisa tera largura do trilho.
scaleXnumérico entra direto.origin-leftsai no CSS compilado(
.origin-left{transform-origin:left}no.next/static/css).Ressalva de fidelidade visual (leia antes de aprovar)
A geometria é idêntica (tabela acima), mas duas coisas rasterizadas passam a acompanhar
a escala não-uniforme da barra, por construção da técnica:
box-shadow(0 0 12px <cor>80) tem o alastramento horizontal multiplicado pelaescala — a 50% ele sangra ~6px em vez de 12px dentro do trilho;
rounded-fullda ponta direita vira uma elipse leve no meio da corrida.Ambos são exatos a 0% e a 100% (
scaleX(1)= identidade — inclusive no estado que ficana tela no fim da corrida), ficam numa barra de 8px de altura e a ponta direita é onde
o 🚀/🏁 (16px, com
drop-shadowpróprio, não escalado) se sobrepõe. Compensar o haloexigiria blur de
12/escalapx — a 1% de progresso, um blur de 1200px, ou seja, trocarlayout por rasterização cara e contrariar o objetivo da issue. Preferi não fazer e declarar.
Gate
typecheck✅ ·build✅ ·vitest158/158 ✅ ·validate-metrics45/45 ✅ ·validate-persistence45/45 ✅ · lint N/A (sem config ESLint no repo).Classificação e riscos
§7.3 normal — só
src/components/RaceTrack.tsx; não tocaapi/rooms/**,supabase.ts,anti-cheat, migrations, CI nem dependências. Segui o escopo do Curador na íntegra; a única
adição minha é o
pointer-events-noneno wrapper do marcador (ele agora cobre o trilhointeiro, e sem isso passaria a interceptar ponteiro onde antes não havia nada).
Risco residual: a ressalva visual acima.
reduced-motionna pista continua como estava —gap pré-existente que o próprio Curador separou desta fatia (é a #108).
📌 Achado fora de escopo, para o Curador:
TypingCore.tsx:252-256animawidthna barrade progresso pessoal — dentro do componente da
textareae retargetada a cada tecla.É o mesmo defeito, no lugar mais sensível possível, e não estava mapeado em nenhuma issue.
Closes #59