Nessuna rotta aveva preload configurato: il chunk JS di una pagina iniziava a scaricarsi solo a tap completato, mai prima. Con 4 tab piccole (8-12KB l'una) e sempre visibili, non c'e' motivo di aspettare il tap per iniziare. preload="intent" sui 4 Link: parte al touchstart, prima che il tap sia completo. Precarica solo il codice, non i dati (nessuna rotta usa loader), quindi nessun costo aggiuntivo di letture verso Supabase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
14 KiB
TODO — Lag su Android (analisi Squadra + BarraSottosezioni)
Contesto: lag segnalato su Android nella pagina Squadra (src/routes/squadra.tsx) e nel
componente BarraSottosezioni (src/components/crapp/BarraSottosezioni.tsx). Elenco delle
cause individuate, in ordine di probabile impatto, con lo stato di applicazione.
1. [x] Ricalcolo di tutte le tab ad ogni render — APPLICATA
voci in Squadra() costruiva il JSX di tutte e 5 le sezioni (Rosa, Statistiche, Classifica,
Obiettivi, Badge) a ogni render, incluse quelle non visibili — inclusi i calcoli per giocatore
(badgeGiocatore, badgeSegretiSbloccati, collezioneBadge) e l'ordinamento della classifica.
Un semplice setAperto (aprire una card giocatore) rifaceva tutto questo lavoro per niente.
Fix: memoizzato il contenuto di ogni tab con useMemo, così il lavoro di ciascuna sezione si
rifà solo quando cambiano i suoi dati effettivi, non ad ogni render del componente.
Nessun trade-off: puro guadagno di performance, identico su Android e iOS.
2. [x] backdrop-blur-md sulla barra tab sticky — APPLICATA
BarraSottosezioni.tsx (variante "pillole", solo Profilo — Squadra/Campionato usano
"sottolineatura" e non avevano blur qui). Costoso da compositare su Android Chrome/WebView,
specialmente con la barra visibile durante lo scroll del pannello sottostante. Su iOS Safari il
costo è molto più basso (accelerazione via Core Animation).
Fix: BarraSottosezioni ora usa useMotoRidotto (stesso heuristic di RAM bassa /
prefers-reduced-motion già introdotto per BottomNav, punto 6) al posto del solo
useReducedMotion. Sui device deboli il blur sparisce (sfondo pieno bg-background); su iOS e
device performanti resta identico a prima.
Presente anche in CelebrazioneBadge.tsx:33 (backdrop-blur-sm), non toccato: non è sticky e
compare solo alla celebrazione, impatto marginale.
3. [x] Gesture drag="x" di Framer Motion sul pannello swipeabile — APPLICATA
BarraSottosezioni.tsx e stesso pattern in calendario.tsx (swipe tra mesi). Ogni pointermove
passa dal thread JS invece di essere gestito nativamente; su Android più pesante da instradare,
specialmente con touch-pan-y + drag="x" insieme su contenuto lungo che scrolla in verticale.
Fix: drag è ora ridotto ? false : "x" in entrambi i file, stesso useMotoRidotto del punto
2. Sui device deboli lo swipe orizzontale (cambio tab / cambio mese) si disattiva e resta solo
il tocco diretto sulla barra/frecce; su iOS e device performanti nulla cambia.
4. [ ] Coriandoli via canvas-confetti non sempre filtrati sui device deboli
lib/motion.ts:14-17,29-40. Già protetti da prefers-reduced-motion e da
deviceMemory < 2GB, ma molti Android di fascia media dichiarano 3-4GB pur avendo GPU deboli:
il check attuale non li intercetta. Impatto marginale (animazione una tantum), ma coerente con
i punti 2 e 3 se si introduce un heuristic più ampio (es. anche hardwareConcurrency).
5. [ ] Nessuna virtualizzazione sulle liste lunghe (rosa, obiettivi, badge)
Non specifico di Android, ma ogni card ha due box-shadow sovrapposte (shadow-card, vedi
styles.css:160-171) e gli obiettivi montano un IntersectionObserver per elemento
(Reveal). Con liste lunghe il costo si somma di più su CPU/GPU Android debole.
Da valutare solo se la rosa/obiettivi crescono molto: oggi la complessità aggiunta (virtualizzazione) non sembra giustificata dal beneficio.
6. [x] Lag nel cambio pagina (Home ↔ Calendario ↔ Squadra ↔ Campionato) — APPLICATA
BottomNav resta montata su ogni rotta (__root.tsx:204-210)
e concentrava due costi proprio nel momento del cambio pagina:
layoutId="capsula-nav"di Framer Motion (BottomNav.tsx) anima la pillola attiva con una misurazione di layout sincrona (FLIP) ad ogni navigazione.- Il "vetro" della barra usa, solo su Blink/Android (
@supports (backdrop-filter: url(...))), una catena di filtri SVGfeTurbulence+feGaussianBlur+feDisplacementMapmolto più pesante del sempliceblur()che riceve iOS Safari — e ricalcola ad ogni cambio di contenuto sotto la barra fissa.
Fix: BottomNav ora usa useMotoRidotto (già in lib/motion.ts, copre sia
prefers-reduced-motion sia RAM bassa) al posto del solo useReducedMotion di Framer Motion.
Quando rileva un device debole, aggiunge data-leggero al contenitore .vetro; una nuova
regola in styles.css disattiva backdrop-filter su quell'attributo (fallback a sfondo pieno,
stesso trattamento già riservato a prefers-reduced-transparency). La molla della capsula era
già condizionata alla stessa variabile ridotto, quindi ora si disattiva anche lei sui device
deboli, non solo con prefers-reduced-motion.
Nessun impatto su iOS/device performanti: lì ridotto resta false e l'effetto vetro + la
capsula animata restano identici a prima.
7. [x] classifica.tsx (Campionato) ricalcolava entrambe le tab ad ogni render — APPLICATA
Stesso pattern di squadra.tsx (punto 1): BarraSottosezioni con le tab "Classifica" e
"Storico partite" costruiva il JSX di entrambe ad ogni render, incluso il merge tra partite CSI
e scout e il lookup MVP per data. Con useCsi/useEventi/useVotiMvp basati su TanStack Query,
un refetch in background di uno solo di questi dati rifaceva comunque il lavoro di tutt'e due
le tab. Meno grave del caso Squadra (qui non c'è uno stato locale tipo aperto che rirenderizza
il componente ad ogni tap), ma stesso spreco ad ogni mount/refetch della pagina.
Fix: stesso trattamento — classifica, tuttiMatch, eventoIdPerData e mvpPerMatch sono ora
memoizzati con useMemo, e il contenuto di ciascuna tab è a sua volta memoizzato sulle relative
dipendenze.
index.tsx (Home) non ha lo stesso problema: non usa BarraSottosezioni/tab multiple, quindi
non c'è contenuto "nascosto" che viene ricalcolato inutilmente — non modificata.
8. [x] Rifiniture minori — APPLICATA
TeamLogo(ui-bits.tsx) non avevadecoding="async"(a differenza diAvatar): aggiunto, guadagno marginale ma gratuito..materialeinstyles.css(backdrop-filter 20px + 3 fallback di accessibilità) non era più referenziata da nessun componente — sostituita da.vetrosulBottomNavin un commit precedente (c089d9e) e da allora orfana. Rimossa insieme ai suoi fallback; il commento che la citava come confronto per.vetroè stato aggiornato.
9. [x] LinkProfilo (header di ogni pagina) tirava dentro tutte le statistiche della rosa
— APPLICATA
ui-bits.tsx (LinkProfilo, usato in PageHeader su quasi ogni rotta) usava useIo, che
internamente chiama useRosa(): quest'ultimo calcola l'intera rosa arricchita (voti MVP,
medie pagelle, statistiche cacche, turni palloni, infortuni/ritardi — 6 hook + un useMemo
pesante su tutta la squadra) solo per leggere id e iniziali di un giocatore.
Fix: LinkProfilo ora usa useGiocatoreBase (lib/user-store.ts, sola anagrafica da
giocatori_squadra) e calcola le iniziali con inizialiDa (ora esportata da crapp-data.ts),
senza toccare gli altri moduli statistici.
Verificato con build reale: le funzioni pesanti (mediePagelle, statisticheCacche,
conteggioTurni, InfortuniERitardi) sono sparite dal chunk ui-bits. Attenzione: la
dimensione totale del chunk non è cambiata (452KB) perché il grosso del peso è l'SDK Supabase
stesso, già necessario a monte in __root.tsx per il gate di login — non rimovibile da qui.
Il guadagno reale di questo fix è sul lavoro a runtime evitato (6 hook + memo in meno ad ogni
render dell'header), non sulla dimensione del bundle.
10. [x] Aprire Calendario montava la stessa rosa "pesante" solo per i compleanni — APPLICATA
calendario.tsx chiamava useRosa() (le stesse 6 statistiche del punto 9: MVP, pagelle,
cacche, palloni, infortuni/ritardi) ma usava il risultato solo per compleanniEventi, che
legge esclusivamente id, nome e nascita. Cliccare Calendario forzava quindi il calcolo (e,
la prima volta nella sessione, il fetch) di dati completamente estranei al calendario — probabile
causa diretta del lag "particolarmente su Calendario" segnalato.
Fix: nuovo hook useAnagraficaRosa in rosa.ts — solo useGiocatoriSquadra + lookup statico
nascitaPerId, senza gli altri 5 hook né il useMemo pesante. compleanniEventi (eventi.ts)
accetta ora Pick<Giocatore, "id" | "nome" | "nascita">[] invece dell'intero Giocatore[],
riflettendo che è tutto ciò che usa.
11. [x] EventoCard ricalcolava la rosa intera una volta per card — APPLICATA
Causa più probabile del lag "ancora presente" su Calendario dopo i punti 9-10: EventoCard
(EventoCard.tsx:72) usava useGiocatoreCorrente
(= useIo = useRosa, le 6 statistiche del punto 9) solo per leggere io.id — mai una
statistica. Il problema si moltiplica perché ogni EventoCard monta il proprio hook: il
Calendario ne renderizza diverse insieme (fino a 3 in "Prossimi eventi", altre in "Compleanni",
altre ancora nel drawer del giorno), quindi apriva Calendario = N ricalcoli indipendenti
dell'intera rosa con tutte le statistiche, non uno solo. Home ha lo stesso pattern (usa
EventoCard per "Prossimo impegno" e "Da confermare").
Fix: EventoCard usa ora useGiocatoreBase (sola anagrafica) invece di useGiocatoreCorrente.
Nota: useGiocatoreCorrente è usato in altri 12 file (PromemoriaPalloni, TurnoPalloni,
Pagelle, RosaPresenze, VotoSocial, VotazioneMvp, ScoutEntry, SondaggioCacche,
CelebrazioneBadge, partita.$id.tsx, eventi.tsx, scout.tsx, benvenuto.tsx) — non
verificati singolarmente in questo giro. Se il lag emergesse altrove, controllare prima se
quell'uso legge davvero un campo statistico (allora serve useIo) o solo l'identità (allora
useGiocatoreBase basta), stesso ragionamento dei punti 9-11.
12. [x] Stesso bug in altri 12 file — APPLICATA
Audit di tutti gli altri usi di useGiocatoreCorrente (oltre EventoCard, punto 11): 12 su 13
leggevano solo .id/.nome/verità, mai una statistica — stesso identico bug, ognuno però
montato una volta sola (non moltiplicato come in EventoCard).
Corretti (→ useGiocatoreBase): benvenuto.tsx, VotazioneMvp.tsx, SondaggioCacche.tsx,
VotoSocial.tsx, eventi.tsx, PromemoriaPalloni.tsx (Home — impatto più alto del gruppo),
TurnoPalloni.tsx, ScoutEntry.tsx, Pagelle.tsx.
partita.$id.tsx:49: io era dichiarato e mai più usato — rimosso del tutto (variabile morta,
nessun downgrade necessario).
Trovati due bonus con lo stesso pattern ma sull'hook useRosa (non useGiocatoreCorrente),
sistemati nello stesso giro:
scout.tsx: siaio(→useGiocatoreBase) siarosa(→useAnagraficaRosa, usava solo id/nome/numero per la selezione live).RosaPresenze.tsx(montata su partita e allenamento): stesso doppio fix.useAnagraficaRosaesteso conruoloenumero(oltre a id/nome/nascita) per coprire anche questo caso.
L'unica eccezione confermata è CelebrazioneBadge.tsx, che usa realmente le statistiche
complete tramite useNotificheSmart — montato globalmente in __root.tsx, quindi resta il
costo di base più alto rimasto in giro, ma non è downgradabile: le servono davvero.
13. [x] Nessuna rotta precaricava il proprio chunk — APPLICATA
router.tsx non impostava defaultPreload, e nessun <Link> aveva preload: il chunk JS di
una pagina (Squadra/Calendario/Campionato: 8-12KB ciascuno) iniziava a scaricarsi solo a tap
completato, mai prima. Per quanto si ottimizzi il lavoro interno di una pagina, resta comunque
un intervallo strutturale tra "tocco la tab" e "vedo il contenuto".
Fix: i 4 link di BottomNav.tsx hanno ora preload="intent" — il chunk parte già al
touchstart, prima che il tap sia completo. Scelto intent e non render (precaricamento
immediato appena la barra compare) perché la BottomNav è sempre visibile: render
aggiungerebbe aggressività senza un vantaggio reale rispetto a intent.
Nota: precarica solo il codice della pagina, non i dati — nessuna rotta usa loader per
prefetchare le query (useEventi, useRispostePresenze, …), quindi nessun costo aggiuntivo di
letture verso Supabase (coerente con il commento "risparmio Cloud" in router.tsx). Se restasse
un flash di caricamento dati visibile, il passo successivo sarebbe wireare un loader per
rotta — cambiamento più strutturale, da valutare solo se necessario.
Considerato e scartato: caricare gli eventi del calendario mese per mese invece che tutti insieme. I 75 eventi sono un payload piccolo (il fetch non è oggi un problema misurabile), "Prossimi eventi" e "Compleanni" leggono oltre il mese corrente (rispettivamente in avanti nel tempo e su tutto l'anno) quindi richiederebbero comunque più mesi in anticipo, e il cambio mese — oggi istantaneo perché filtra dati già in memoria — diventerebbe la parte più lenta invece che la più fluida.
Note
useMotoRidotto(lib/motion.ts) è ora l'heuristic condiviso di "device debole" usato inBottomNav.tsx,BarraSottosezioni.tsxecalendario.tsx(punti 2, 3, 6). Se si riprende il punto 4 (coriandoli), conviene usare lo stesso hook invece di un check separato.- Applicati: 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13. Restano da discutere/prioritizzare: 4
(coriandoli su device medi), 5 (virtualizzazione liste lunghe),
CelebrazioneBadge.tsx(usa legittimamente le statistiche complete ma è montato su ogni pagina — non downgradabile, eventualmente da rivedere con un intervento diverso, es. memoizzazione più aggressiva), e un eventualeloaderper rotta per prefetchare anche i dati (non solo il codice, punto 13). - Pattern ricorrente (punti 9, 10, 11):
useRosa()/useIo()/useGiocatoreCorrente()sono comodi ma calcolano tutte le statistiche della squadra; usarli solo per identità/anagrafica (id, nome, nascita, iniziali) costa 5-6 hook e unuseMemosu tutta la rosa inutilmente, e il costo si moltiplica per ogni componente che lo monta (punto 11). Per la sola identità:useGiocatoreBase(singolo giocatore) ouseAnagraficaRosa(tutta la rosa). - Se il lag persistesse ancora dopo questi fix, il prossimo passo è profilare un device Android
reale (Chrome DevTools remoto o
chrome://inspect) invece di continuare a ipotizzare: a questo punto le cause "ovvie" lette dal codice sono coperte, e senza un trace reale si rischia di ottimizzare a caso.