Skip to content

Performance/Área sagrada: o relógio do countdown nunca desliga — 10 Hz de re-render de RoomView/Race/RaceTrack durante a corrida inteira (useRoom.ts:411-418) #111

Description

@caioross

Contexto

O relógio do countdown é criado uma vez e nunca é desligado quando o countdown acaba
ele continua batendo a 10 Hz durante a corrida inteira, re-renderizando a árvore da sala
sem que nada mude na tela.

src/lib/useRoom.ts:411-418:

// Tick a clock only while a countdown is pending.
const startMs = room?.start_at ? Date.parse(room.start_at) : 0;
useEffect(() => {
  if (room?.status !== "racing" || !startMs) return;
  if (Date.now() >= startMs) return;          // guarda só de MONTAGEM
  const t = setInterval(() => setNow(Date.now()), 100);
  return () => clearInterval(t);
}, [room?.status, startMs]);                  // ← nenhuma das duas muda no start

A guarda de :415 só roda quando o efeito roda. As deps são room?.status e startMs:
durante toda a corrida o status permanece "racing" e startMs é o mesmo número, então o
efeito não re-executa e o clearInterval só chega quando o status vira "finished"
(ou no unmount). Entre o fim do countdown e o fim da corrida, o intervalo bate ~10×/s.

O trabalho é 100% desperdício. now (:54) tem um único consumidor — countdownN
(:466-469) — que devolve null assim que now >= startMs. Cada um desses ~10 renders/s
produz uma árvore idêntica à anterior.

E ele alcança a área sagrada (HANDBOOK §2). RoomView não é memo e re-renderiza a cada
setNow; dentro dele, durante a corrida, Race (RoomView.tsx:120) e RaceTrack
também não são memo (Race.tsx:10, RaceTrack.tsx:17 — só TypingCore, CodeDisplay
e CodeEditor ganharam memo na #105). Ou seja: RoomView + Race + RaceTrack (até 30
linhas de jogador, room.ts:282) reconciliam 10×/s na mesma main thread da textarea.

Não é a #59 nem a fatia 2 dela. A #59 ataca custo proporcional ao nº de jogadores
(mensagens de progresso alheio) e a fatia 2 é width/leftscaleX/translateX em
RaceTrack. Este aqui é constante, 10 Hz, independente de quantos jogadores existem
acontece até numa sala de 1 pessoa. As duas correções são ortogonais.

Quem é atingido: todo mundo que estava presente no countdown, ou seja, todos os
competidores de verdade. Quem entra depois do start cai na guarda de :415 e escapa —
espectador (#64) não paga, competidor paga.

Acceptance criteria

  1. Passado o instante de start_at, nenhum setNow continua agendado enquanto
    status === "racing". Verificável instrumentando o setter (contador de chamadas).
  2. O countdown visual permanece idêntico: Countdown n conta 3→2→1 com a mesma cadência,
    inclusive para quem entra durante o countdown (o caminho de :415 que hoje funciona
    não pode regredir).
  3. Medição antes/depois no PR, mesmo método da perf(race): corta re-render de CodeDisplay/CodeEditor por progresso alheio (#59, fatia 1) #105: renders de RoomView numa janela de
    10 s de corrida ociosa (ninguém digitando). Hoje ≈ 100; alvo ≈ 0 vindos do relógio.
  4. Sala que termina e volta ao lobby (reset) e depois começa nova corrida volta a contar
    o countdown normalmente — o relógio tem de religar, não morrer de vez.
  5. Sem tocar em PROGRESS_THROTTLE_MS, broadcastProgress, maybeFinish ou
    RACE_DECISION_TICK_MS.
  6. Gate verde: pnpm typecheck && pnpm build, vitest, validate-metrics,
    validate-persistence.

Dica de abordagem

Duas saídas simples, ambas cabendo em poucas linhas de useRoom.ts:

  • Auto-encerrar o intervalo: no próprio tick, se Date.now() >= startMs, dar um último
    setNow (para o countdownN virar null) e clearInterval ali mesmo; ou
  • Trocar por setTimeout encadeado / um timeout único para o instante exato do start,
    já que o alvo é conhecido (startMs) e o número de ticks é finito.

Prefira a variante que mantenha a guarda de montagem de :415 intacta (é ela que poupa o
espectador). Cuidado com o caso startMs no passado por relógio dessincronizado: o efeito
não pode virar um loop de setNow.

Classificação: §7.3 normal — só src/lib/useRoom.ts; não toca api/rooms/** nem
src/lib/supabase.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Prioridade normalarea:multiplayerSalas, Realtime, presença, leaderboard, persistênciaarea:uiInterface, design system, animação, game feel

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions