Scopri come creare liste veloci per cruscotti con 100k righe usando paginazione, virtualizzazione, filtri intelligenti e query ottimizzate per mantenere gli strumenti interni reattivi.

Una schermata lista di solito sembra ok finché non smette di esserlo. Gli utenti cominciano a notare piccoli blocchi che si sommano: lo scorrimento balbetta, la pagina si blocca un attimo dopo ogni aggiornamento, i filtri impiegano secondi a rispondere e vedi uno spinner dopo ogni click. A volte la scheda del browser pare congelata perché il thread UI è occupato.
100k righe è un punto di svolta comune perché mette sotto stress tutte le parti del sistema insieme. Il dataset è ancora normale per un database, ma è abbastanza grande da rendere evidenti piccole inefficienze nel browser e sulla rete. Se provi a mostrare tutto in una volta, una schermata semplice diventa una pipeline pesante.
L’obiettivo non è renderizzare tutte le righe. L’obiettivo è aiutare qualcuno a trovare quello che serve rapidamente: le 50 righe giuste, la pagina successiva o una fetta stretta filtrata.
Conviene dividere il lavoro in quattro parti:
Se anche una sola parte è costosa, l’intera schermata sembra lenta. Una semplice casella di ricerca può attivare una richiesta che ordina 100k righe, restituisce migliaia di record e poi forza il browser a renderizzarli tutti. Ecco perché digitare diventa lento.
Quando i team costruiscono strumenti interni rapidamente (anche con piattaforme low-code come Koder.ai), le schermate lista sono spesso il primo posto dove la crescita reale dei dati espone il divario tra “funziona su dataset demo” e “sembra istantaneo tutti i giorni”.
Prima di ottimizzare, decidi cosa significa “veloce” per questa schermata. Molti team inseguono il throughput (caricare tutto) quando gli utenti hanno principalmente bisogno di bassa latenza (vedere qualcosa aggiornarsi rapidamente). Una lista può sembrare istantanea anche se non carica mai tutte le 100k righe, purché risponda rapidamente a scorrimento, ordinamento e filtri.
Un obiettivo pratico è il tempo alla prima riga, non il tempo per caricare tutto. Gli utenti si fidano della pagina quando vedono le prime 20–50 righe in fretta e le interazioni restano fluide.
Scegli un piccolo set di numeri da tracciare ogni volta che cambi qualcosa:
COUNT(*) e SELECT ampi)Questi si mappano ai sintomi comuni. Se la CPU del browser impazzisce quando scorri, il frontend sta facendo troppo lavoro per riga. Se lo spinner aspetta ma lo scorrimento è ok dopo, di solito il problema è backend o rete. Se la richiesta è veloce ma la pagina si blocca, quasi sempre è rendering o pesante elaborazione client-side.
Prova questo esperimento: mantieni la UI identica, ma limita temporaneamente il backend a restituire solo 20 righe con gli stessi filtri. Se diventa veloce, il collo di bottiglia è la dimensione del caricamento o il tempo della query. Se resta lento, guarda il rendering, il formatting e i componenti per riga.
Esempio: una schermata Orders interna sembra lenta quando digiti nella ricerca. Se l’API restituisce 5.000 righe e il browser le filtra a ogni battitura, la digitazione laggherà. Se l’API impiega 2 secondi a causa di una query COUNT su un filtro non indicizzato, vedrai l’attesa prima che qualsiasi riga cambi. Fix diversi, stessa lamentela utente.
Il browser è spesso il primo collo di bottiglia. Una lista può sembrare lenta anche quando l’API è veloce, semplicemente perché la pagina sta cercando di dipingere troppo. La prima regola è semplice: non renderizzare migliaia di righe nel DOM tutte insieme.
Anche prima di aggiungere la virtualizzazione completa, mantieni ogni riga leggera. Una riga con wrapper annidati, icone, tooltip e stili condizionali complessi in ogni cella ti costa a ogni scroll e a ogni aggiornamento. Preferisci testo semplice, un paio di badge piccoli e solo uno o due elementi interattivi per riga.
Un’altezza riga stabile aiuta più di quanto sembri. Quando ogni riga ha la stessa altezza, il browser può prevedere il layout e lo scorrimento resta fluido. Le righe con altezza variabile (descrizioni che vanno a capo, note espandibili, avatar grandi) attivano misurazioni aggiuntive e reflow. Se ti servono dettagli extra, considera un pannello laterale o un’area espandibile singola, non una riga multi-linea completa.
La formattazione è un’altra tassa silenziosa. Date, valute e pesante lavoro su stringhe si sommano quando ripetuti in molte celle.
Se un valore non è visibile, non calcolarlo ancora. Cache i risultati di formattazione costosi e calcolali su richiesta, per esempio quando una riga diventa visibile o quando l’utente apre una riga.
Un rapido intervento che spesso dà un guadagno tangibile:
Esempio: una tabella Invoices interna che formatta 12 colonne di valute e date balbetterà nello scroll. Mettere in cache i valori formattati per fattura e ritardare il lavoro per le righe off-screen può farla sembrare istantanea, anche prima di interventi più profondi sul backend.
La virtualizzazione fa sì che la tabella disegni solo le righe che puoi effettivamente vedere (più un piccolo buffer sopra e sotto). Mentre scorri, riusa gli stessi elementi DOM e sostituisce i dati dentro di essi. Questo evita che il browser cerchi di dipingere decine di migliaia di componenti riga contemporaneamente.
La virtualizzazione è adatta quando hai liste lunghe, tabelle larghe o righe pesanti (avatar, chip di stato, menu di azione, tooltip). È utile anche quando gli utenti scorrono molto e si aspettano una vista continua e fluida anziché spostarsi pagina per pagina.
Non è magia. Alcune cose spesso causano sorprese:
L’approccio più semplice è noioso: altezza riga fissa, colonne prevedibili e non troppi widget interattivi dentro ogni riga.
Puoi combinare entrambi: usa paginazione (o caricamento con cursore) per limitare ciò che prendi dal server e virtualizzazione per tenere economico il rendering dentro la fetta scaricata.
Un pattern pratico è prelevare una pagina normale (spesso 100–500 righe), virtualizzare all’interno di quella pagina e offrire controlli chiari per spostarsi tra le pagine. Se usi lo scroll infinito, aggiungi un indicatore visibile “Caricate X di Y” così gli utenti capiscono che non stanno vedendo tutto.
Se ti serve una lista che rimanga utilizzabile mentre i dati crescono, la paginazione è di solito il default più sicuro. È prevedibile, funziona bene per flussi admin (review, edit, approve) e supporta bisogni comuni come esportare “pagina 3 con questi filtri” senza sorprese. Molti team tornano alla paginazione dopo aver provato scroll più sofisticati.
Lo scroll infinito può sembrare gradevole per la navigazione casuale, ma ha costi nascosti. Le persone perdono il senso del punto in cui si trovano, il pulsante indietro spesso non riporta allo stesso posto e sessioni lunghe possono accumulare memoria man mano che si caricano più righe. Un compromesso è un pulsante Carica altro che comunque usa pagine, così l’utente resta orientato.
L’offset (page=10&size=50) è il classico approccio. È semplice, ma può rallentare su tabelle grandi perché il database potrebbe dover scorrere molte righe per raggiungere pagine successive. Inoltre può risultare strano quando arrivano nuove righe e gli elementi si spostano tra le pagine.
La keyset (cursor) pagination chiede “le prossime 50 righe dopo l’ultimo elemento visto”, di solito usando un id o created_at. Tende a restare veloce perché non deve contare o saltare molte righe.
Regola pratica:
Agli utenti piace vedere i totali, ma un “count di tutte le righe corrispondenti” può essere costoso con filtri pesanti. Opzioni includono cache dei conteggi per filtri popolari, aggiornare il conteggio in background dopo il caricamento della pagina o mostrare un conteggio approssimato (es. “10.000+”).
Esempio: una schermata Orders interna può mostrare risultati istantanei con paginazione keyset, poi completare il totale esatto solo quando l’utente smette di cambiare filtri per un secondo.
Se stai costruendo questo in Koder.ai, tratta la paginazione e il comportamento dei conteggi come parte della specifica della schermata fin dall’inizio, così le query generate dal backend e lo stato UI non si pestano i piedi dopo.
La maggior parte delle liste sembra lenta perché parte troppo aperta: carica tutto, poi chiede all’utente di restringere. Capovolgi l’approccio. Parti con valori sensati che restituiscono un set piccolo e utile (per esempio: ultimi 7 giorni, My items, Status: Open) e fai di “Tutto il periodo” una scelta esplicita.
La ricerca testuale è un altro tranello comune. Se lanci una query a ogni battitura, crei un backlog di richieste e una UI che sfarfalla. Debounce l’input di ricerca in modo da interrogare solo dopo che l’utente ha fatto una breve pausa e annulla le richieste vecchie quando ne parte una nuova. Regola semplice: se l’utente sta ancora digitando, non colpire il server.
I filtri funzionano solo quando sono chiari. Mostra le chip dei filtri in cima alla tabella così l’utente vede cosa è attivo e può rimuoverlo con un clic. Usa etichette umane, non nomi di campo grezzi (per esempio, Owner: Sam invece di owner_id=42). Quando qualcuno dice “i miei risultati sono scomparsi”, di solito c’è un filtro invisibile.
Pattern che mantengono le liste grandi reattive senza complicare l’UI:
Le viste salvate sono l’eroe silenzioso. Invece di insegnare agli utenti a creare ogni volta combinazioni di filtri ad-hoc, dai loro preset che rispecchiano flussi reali. Un team ops potrebbe alternare Failed payments today e High-value customers. Quelli possono esserci con un click, comprensibili istantaneamente e più semplici da mantenere veloci sul backend.
Se costruisci uno strumento interno in un builder chat-driven come Koder.ai, tratta i filtri come parte del flusso prodotto, non come un’aggiunta. Parti dalle domande più comuni, poi progetta la vista predefinita e le viste salvate attorno a quelle.
Una schermata lista raramente ha bisogno degli stessi dati di una pagina dettaglio. Se la tua API restituisce tutto su tutto, paghi due volte: il database fa più lavoro e il browser riceve e rende più di quanto serva. La modellazione delle query è l’abitudine di richiedere solo ciò che la lista serve in quel momento.
Inizia restituendo solo le colonne necessarie per rendere ogni riga. Per la maggior parte dei cruscotti sono sufficienti id, un paio di etichette, uno stato, un owner e timestamp. Testi lunghi, blob JSON e campi calcolati possono aspettare che l’utente apra la riga.
Evita join pesanti per la prima pittura. I join vanno bene quando usano indici e restituiscono risultati piccoli, ma diventano costosi quando unisci molte tabelle e poi ordini o filtri sui dati uniti. Un pattern semplice: prendi la lista da una tabella velocemente, poi carica i dettagli correlati on demand (o batch-load solo per le righe visibili).
Limita le opzioni di ordinamento e ordina su colonne indicizzate. “Ordina per qualsiasi colonna” sembra utile, ma spesso forza ordinamenti lenti su dataset grandi. Preferisci poche scelte prevedibili come created_at, updated_at o status, e assicurati che quelle colonne siano indicizzate.
Stai attento con l’aggregazione server-side. COUNT(*) su un set filtrato enorme, DISTINCT su una colonna ampia o i calcoli delle pagine totali possono dominare il tempo di risposta.
Approccio pratico:
COUNT e DISTINCT come opzionali; mettili in cache o approssimali quando possibileSe costruisci strumenti interni su Koder.ai, definisci una query lista leggera separata dalla query dettaglio in fase di pianificazione, così l’UI resta reattiva man mano che i dati crescono.
Se vuoi una lista che resti veloce a 100k righe, il database deve fare meno lavoro per richiesta. La maggior parte delle liste lente non è “troppi dati”, ma il pattern di accesso sbagliato.
Inizia con indici che corrispondono a ciò che gli utenti fanno davvero. Se la tua lista è solitamente filtrata per status e ordinata per created_at, ti serve un indice che supporti entrambi, in quell’ordine. Altrimenti il database potrebbe scansionare molte più righe del previsto e poi ordinarle, cosa che diventa costosa in fretta.
Fix che di solito portano i maggiori guadagni:
tenant_id, status, created_at).OFFSET profondo. OFFSET fa camminare il DB passando molte righe solo per saltarle.Esempio semplice: una tabella Orders interna che mostra nome cliente, stato, importo e data. Non fare join con tutte le tabelle correlate e non tirare fuori tutte le note d’ordine per la vista lista. Restituisci solo le colonne usate nella tabella e carica il resto in una richiesta separata quando l’utente clicca su un ordine.
Se costruisci con una piattaforma come Koder.ai, mantieni questa mentalità anche se l’interfaccia è generata dalla chat. Assicurati che gli endpoint API generati supportino paginazione a cursore e campi selezionabili, così il lavoro del database resta prevedibile mentre la tabella cresce.
Se una pagina lista sembra lenta oggi, non iniziare riscrivendo tutto. Parti definendo cosa è uso “normale”, poi ottimizza quel percorso.
Definisci la vista predefinita. Scegli filtri predefiniti, ordine e colonne visibili. Le liste rallentano quando cercano di mostrare tutto per default.
Scegli uno stile di paging che corrisponda all’uso. Se gli utenti scansionano soprattutto le prime pagine, la paginazione classica va bene. Se saltano in profondità (pagina 200+) o hai bisogno di performance stabili a qualsiasi profondità, usa keyset pagination (basata su un ordinamento stabile come created_at più un id).
Aggiungi virtualizzazione per il corpo della tabella. Anche se il backend è veloce, il browser può soffocare quando renderizza troppe righe insieme.
Rendi ricerca e filtri reattivi. Debounce sulla digitazione così non spari richieste a ogni tasto. Conserva lo stato dei filtri nell’URL o in uno store condiviso in modo che refresh, back button e condivisione della vista funzionino. Metti in cache l’ultimo risultato riuscito così la tabella non lampeggia vuota.
Misura, poi affina query e indici. Logga tempo server, tempo database, dimensione del payload e tempo di render. Poi sgronda la query: seleziona solo le colonne che mostri, applica i filtri presto e aggiungi indici che corrispondano al filtro + ordinamento predefinito.
Esempio: un cruscotto support con 100k ticket. Default a Open, assegnati al mio team, ordinati per i più recenti, mostra sei colonne e preleva solo ticket id, subject, assignee, status e timestamp. Con paginazione keyset e virtualizzazione, mantieni sia il database sia l’UI prevedibili.
Se costruisci strumenti interni in Koder.ai, questo piano si sposa bene a un flusso iterate-and-check: aggiusta la vista, testa scorrimento e ricerca, poi affina la query finché la pagina resta reattiva.
Il modo più veloce per far sembrare una lista rotta è trattare 100k righe come una pagina normale. La maggior parte delle dashboard lente ha alcune trappole prevedibili.
Una grossa è renderizzare tutto e nasconderlo con CSS. Anche se sembra che solo 50 righe siano visibili, il browser paga comunque per creare 100k nodi DOM, misurarli e repaintare nello scroll. Se ti servono liste lunghe, rendi solo ciò che si vede (virtualizzazione) e mantieni i componenti riga semplici.
La ricerca può anche rovinare le performance quando ogni battitura scatena una scansione dell’intera tabella. Questo succede quando i filtri non sono indicizzati, quando cerchi su troppe colonne o quando fai query di contains su campi testuali grandi senza piano. Una buona regola: il primo filtro che l’utente raggiunge dovrebbe essere economico in DB, non solo comodo in UI.
Un altro problema comune è recuperare record completi quando la lista chiede solo riassunti. Una riga di lista di solito necessita di 5–12 campi, non dell’oggetto completo, non di descrizioni lunghe e non di dati correlati. Tirare dati extra aumenta lavoro DB, tempo di rete e parsing sul frontend.
Esportazioni e totali possono bloccare l’UI se li calcoli sul main thread o aspetti una richiesta pesante prima di rispondere. Mantieni l’UI interattiva: avvia esportazioni in background, mostra il progresso ed evita di ricalcolare totali a ogni cambio filtro.
Infine, troppe opzioni di ordinamento possono ritorcersi contro. Se gli utenti possono ordinare per qualsiasi colonna, finirai per ordinare grandi result set in memoria o costringere il DB in piani lenti. Limita gli ordinamenti a poche colonne indicizzate e fai in modo che l’ordinamento di default corrisponda a un indice reale.
Controllo rapido:
Tratta le prestazioni della lista come una feature di prodotto, non come una correzione una tantum. Una schermata lista è veloce solo quando sembra veloce mentre persone reali scorrono, filtrano e ordinano su dati reali.
Usa questa checklist per confermare di aver sistemato le cose giuste:
Un controllo di realtà: apri la lista, scorri per 10 secondi, poi applica un filtro comune (es. Status: Open). Se l’UI si blocca, il problema è quasi sempre rendering (troppe righe DOM) o una trasformazione client-side pesante (ordinamento, raggruppamento, formattazione) che scatta a ogni aggiornamento.
Prossimi passi, in ordine, così non rimbalzi tra fix diversi:
Se costruisci questo con Koder.ai (koder.ai), parti da Planning Mode: definisci esattamente colonne della lista, campi filtro e shape della risposta prima. Poi itera usando snapshot e rollback quando un esperimento rallenta lo schermo.
Cambia l’obiettivo da “caricare tutto” a “mostrare subito le righe utili”. Ottimizza il tempo alla prima riga e l’interazione liscia durante filtraggio, ordinamento e scorrimento, anche se l’intero dataset non viene mai caricato in una volta sola.
Misura il tempo alla prima riga dopo il caricamento o un cambio filtro, il tempo che impiega un filtro/ordinamento a mostrare i risultati aggiornati, la dimensione della risposta (payload JSON), le query lente in database (soprattutto SELECT ampi e COUNT(*)) e i picchi sul main-thread del browser. Questi numeri corrispondono direttamente a ciò che gli utenti percepiscono come “lag”.
Limita temporaneamente l’API a restituire solo 20 righe con gli stessi filtri e ordinamenti. Se diventa veloce, stai pagando per il costo della query o per la dimensione del payload; se resta lenta, il collo di bottiglia è probabilmente il rendering, il formatting o il lavoro per riga sul client.
Non renderizzare migliaia di righe nel DOM contemporaneamente, tieni i componenti riga semplici e preferisci un’altezza riga fissa. Evita di applicare formattazioni pesanti per righe off-screen: calcola e memorizza le formattazioni solo quando una riga diventa visibile o viene aperta.
La virtualizzazione mantiene montate solo le righe visibili (più un piccolo buffer) riutilizzando gli stessi elementi DOM mentre scorri. Conviene quando gli utenti scorrono molto o le righe sono “pesanti”, ma funziona al meglio con altezza riga consistente e layout prevedibile.
Per la maggior parte dei flussi amministrativi la paginazione è il default più sicuro: mantiene l’orientamento dell’utente e limita il lavoro del server. Lo scroll infinito può andar bene per navigazione casuale, ma spesso complica la navigazione e l’uso della memoria a meno che non gestisci chiaramente lo stato e i limiti.
La paginazione con offset (page=10&size=50) è semplice ma può rallentare nelle pagine profonde perché il database salta molte righe. La paginazione keyset (cursor) restituisce le righe “dopo l’ultimo elemento visto”, di solito usando un id o created_at, e tende a restare veloce perché evita di scorrere le righe saltate.
Non eseguire una query a ogni battitura. Usa il debounce, annulla le richieste in corso quando ne parte una nuova e imposta filtri predefiniti che restringono subito (es. ultimi 7 giorni, My items, Status: Open) in modo che la prima query sia piccola e utile.
Restituisci solo i campi di cui la lista ha bisogno (di solito id, etichetta, stato, assegnatario e timestamp). Sposta testo lungo, blob JSON e dati correlati alla richiesta dettaglio così che la prima pittura resti leggera e prevedibile.
Fai combaciare il filtro e l’ordinamento predefiniti con l’uso reale, poi aggiungi indici che supportino proprio quel pattern (spesso un indice composito). Considera i totali esatti opzionali: calcolarli può essere costoso; mettili in cache, precomputali o mostra valori approssimati quando non è necessario il numero esatto.