High Performance JavaScript ListView nel 2026: Tecniche di Virtualizzazione e Ottimizzazione
Scopri come sviluppare ListView ad alte prestazioni con JavaScript list virtualization e React Virtualized. Deep-dive nelle tecniche di ottimizzazione del rendering.

Nelle applicazioni web moderne, la visualizzazione performante di grandi quantità di dati rappresenta una delle sfide più impegnative. Che si tratti di cataloghi e-commerce, feed social o dashboard analitiche, la capacità di renderizzare migliaia di elementi in modo fluido determina il successo o il fallimento di un'applicazione. La virtualizzazione delle ListView JavaScript si è affermata come tecnica fondamentale per affrontare questa sfida.
La virtualizzazione delle liste può ridurre il consumo di memoria fino al 95% e abbattere il tempo di rendering iniziale da diversi secondi a meno di 100ms, una differenza cruciale per l'esperienza utente.
Il Problema: Sovraccarico del DOM con Liste Estese
Prima di esaminare le soluzioni, è essenziale comprendere il problema fondamentale. Quando un'applicazione tenta di renderizzare 10.000 elementi di lista direttamente nel DOM, emergono problemi di performance significativi:
- Consumo di memoria: Ogni elemento DOM richiede memoria per stili, event listener e informazioni di layout
- Tempo di rendering: Il browser deve calcolare il layout e dipingere ogni elemento
- Performance di scrolling: Durante gli eventi di scroll, il browser deve eseguire reflow continui
// Approccio naive - NON usare per liste estese
interface ListItem {
id: string;
title: string;
description: string;
}
function NaiveList({ items }: { items: ListItem[] }) {
return (
<div className="list-container">
{items.map(item => (
<div key={item.id} className="list-item">
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</div>
);
}
// Con 10.000 elementi: ~2-3 secondi di rendering, 500MB+ di memoriaVirtualizzazione: Il Concetto della Tecnica a Finestra
La virtualizzazione si basa su un principio semplice ma efficace: vengono renderizzati solo gli elementi attualmente visibili nel viewport, più un piccolo buffer sopra e sotto. Questa tecnica è spesso chiamata "windowing" o "tecnica a finestra".
// Principio base della virtualizzazione
interface VirtualListConfig {
totalItems: number;
itemHeight: number;
containerHeight: number;
overscan: number; // Elementi aggiuntivi fuori dal viewport
}
function calculateVisibleRange(
scrollTop: number,
config: VirtualListConfig
): { startIndex: number; endIndex: number } {
const { totalItems, itemHeight, containerHeight, overscan } = config;
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(
totalItems - 1,
startIndex + visibleCount + overscan * 2
);
return { startIndex, endIndex };
}Implementazione di una Lista Virtuale con React
L'implementazione pratica richiede diversi componenti: un container che gestisce lo scrolling e una logica di rendering che visualizza solo gli elementi visibili.
import { useState, useCallback, useRef, useEffect } from "react";
interface VirtualListProps<T> {
items: T[];
itemHeight: number;
containerHeight: number;
renderItem: (item: T, index: number) => React.ReactNode;
overscan?: number;
}
function VirtualList<T extends { id: string }>({
items,
itemHeight,
containerHeight,
renderItem,
overscan = 3
}: VirtualListProps<T>) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const totalHeight = items.length * itemHeight;
const { startIndex, endIndex } = calculateVisibleRange(scrollTop, {
totalItems: items.length,
itemHeight,
containerHeight,
overscan
});
const visibleItems = items.slice(startIndex, endIndex + 1);
const offsetY = startIndex * itemHeight;
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: "auto" }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: "relative" }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
style={{ height: itemHeight }}
>
{renderItem(item, startIndex + index)}
</div>
))}
</div>
</div>
</div>
);
}Ottimizzazione della Performance di Scroll
L'implementazione base funziona, ma può causare stuttering durante lo scroll veloce. Diverse tecniche di ottimizzazione migliorano significativamente la performance:
Passive Event Listener e requestAnimationFrame
function useOptimizedScroll(
callback: (scrollTop: number) => void
) {
const rafRef = useRef<number | null>(null);
const lastScrollTop = useRef(0);
const handleScroll = useCallback((e: Event) => {
const target = e.target as HTMLDivElement;
lastScrollTop.current = target.scrollTop;
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(() => {
callback(lastScrollTop.current);
rafRef.current = null;
});
}
}, [callback]);
useEffect(() => {
return () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
}
};
}, []);
return { handleScroll, options: { passive: true } };
}Memoization degli Elementi della Lista
import { memo, useMemo } from "react";
interface MemoizedItemProps<T> {
item: T;
index: number;
renderItem: (item: T, index: number) => React.ReactNode;
}
const MemoizedListItem = memo(function MemoizedListItem<T>({
item,
index,
renderItem
}: MemoizedItemProps<T>) {
return <>{renderItem(item, index)}</>;
}, (prevProps, nextProps) => {
return prevProps.item === nextProps.item
&& prevProps.index === nextProps.index;
});
// Utilizzo con riferimenti stabili
function OptimizedVirtualList<T extends { id: string }>(props: VirtualListProps<T>) {
const stableRenderItem = useMemo(
() => props.renderItem,
[props.renderItem]
);
// ... resto dell'implementazione
}Altezze Dinamiche: Una Sfida Avanzata
Nella realtà, gli elementi della lista spesso hanno altezze diverse. Questo richiede un'implementazione più complessa con una cache delle altezze:
interface DynamicHeightCache {
heights: Map<string, number>;
estimatedHeight: number;
measuredCount: number;
}
function useDynamicHeights(estimatedHeight: number): {
cache: DynamicHeightCache;
measureElement: (id: string, element: HTMLElement | null) => void;
getHeight: (id: string) => number;
getTotalHeight: (items: { id: string }[]) => number;
getItemOffset: (items: { id: string }[], index: number) => number;
} {
const cacheRef = useRef<DynamicHeightCache>({
heights: new Map(),
estimatedHeight,
measuredCount: 0
});
const measureElement = useCallback((id: string, element: HTMLElement | null) => {
if (!element) return;
const height = element.getBoundingClientRect().height;
const cache = cacheRef.current;
if (!cache.heights.has(id)) {
cache.measuredCount++;
}
cache.heights.set(id, height);
// Aggiorna dinamicamente l'altezza stimata
if (cache.measuredCount > 0) {
let totalMeasured = 0;
cache.heights.forEach(h => totalMeasured += h);
cache.estimatedHeight = totalMeasured / cache.measuredCount;
}
}, []);
const getHeight = useCallback((id: string): number => {
return cacheRef.current.heights.get(id) ?? cacheRef.current.estimatedHeight;
}, []);
const getTotalHeight = useCallback((items: { id: string }[]): number => {
return items.reduce((total, item) => total + getHeight(item.id), 0);
}, [getHeight]);
const getItemOffset = useCallback((items: { id: string }[], index: number): number => {
let offset = 0;
for (let i = 0; i < index; i++) {
offset += getHeight(items[i].id);
}
return offset;
}, [getHeight]);
return {
cache: cacheRef.current,
measureElement,
getHeight,
getTotalHeight,
getItemOffset
};
}Integrazione con TanStack Virtual
Per le applicazioni in produzione, si consiglia l'utilizzo di librerie consolidate come TanStack Virtual (precedentemente React Virtual). Queste offrono implementazioni più mature con funzionalità aggiuntive:
import { useVirtualizer } from "@tanstack/react-virtual";
function TanStackVirtualList<T extends { id: string }>({
items,
estimateSize = 50
}: {
items: T[];
estimateSize?: number;
}) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan: 5,
getItemKey: (index) => items[index].id
});
return (
<div
ref={parentRef}
className="h-[600px] overflow-auto"
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative"
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`
}}
>
<ListItemComponent item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}Integrazione Backend con NestJS: Paginazione Efficiente
Una ListView performante richiede anche un backend ottimizzato. NestJS offre soluzioni eleganti per la paginazione basata su cursore, che si integra meglio con le liste virtualizzate rispetto agli approcci basati su offset:
import { Injectable } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
interface CursorPaginationParams {
cursor?: string;
limit: number;
direction: "forward" | "backward";
}
interface PaginatedResult<T> {
items: T[];
nextCursor: string | null;
prevCursor: string | null;
hasMore: boolean;
}
@Injectable()
export class ItemsService {
constructor(private prisma: PrismaService) {}
async getPaginatedItems(
params: CursorPaginationParams
): Promise<PaginatedResult<Item>> {
const { cursor, limit, direction } = params;
const items = await this.prisma.item.findMany({
take: direction === "forward" ? limit + 1 : -(limit + 1),
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: "desc" }
});
const hasMore = items.length > limit;
const resultItems = hasMore ? items.slice(0, limit) : items;
return {
items: resultItems,
nextCursor: hasMore ? resultItems[resultItems.length - 1].id : null,
prevCursor: cursor ?? null,
hasMore
};
}
}import { Controller, Get, Query } from "@nestjs/common";
import { ItemsService } from "./items.service";
@Controller("items")
export class ItemsController {
constructor(private itemsService: ItemsService) {}
@Get()
async getItems(
@Query("cursor") cursor?: string,
@Query("limit") limit = 50
) {
return this.itemsService.getPaginatedItems({
cursor,
limit: Math.min(limit, 100), // Massimo 100 elementi
direction: "forward"
});
}
}Combinare Infinite Scrolling con Virtualizzazione
La combinazione di infinite scrolling e virtualizzazione permette di caricare e visualizzare quantità di dati praticamente illimitate:
import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
function InfiniteVirtualList() {
const parentRef = useRef<HTMLDivElement>(null);
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ["items"],
queryFn: async ({ pageParam }) => {
const response = await fetch(
`/api/items?cursor=${pageParam ?? ""}&limit=50`
);
return response.json();
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: null as string | null
});
const allItems = useMemo(
() => data?.pages.flatMap(page => page.items) ?? [],
[data]
);
const virtualizer = useVirtualizer({
count: hasNextPage ? allItems.length + 1 : allItems.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5
});
// Caricamento automatico
useEffect(() => {
const virtualItems = virtualizer.getVirtualItems();
const lastItem = virtualItems[virtualItems.length - 1];
if (
lastItem &&
lastItem.index >= allItems.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
}, [virtualizer.getVirtualItems(), hasNextPage, isFetchingNextPage]);
return (
<div ref={parentRef} className="h-screen overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative"
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const isLoaderRow = virtualItem.index >= allItems.length;
return (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualItem.start}px)`
}}
>
{isLoaderRow ? (
<LoadingSpinner />
) : (
<ItemCard item={allItems[virtualItem.index]} />
)}
</div>
);
})}
</div>
</div>
);
}Pronto a superare i tuoi colloqui su Node.js / NestJS?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Metriche di Performance e Monitoring
Per misurare l'efficacia della virtualizzazione, è importante raccogliere metriche rilevanti:
function useVirtualListMetrics() {
const metricsRef = useRef({
renderCount: 0,
totalRenderTime: 0,
maxRenderedItems: 0
});
const trackRender = useCallback((renderedCount: number, renderTime: number) => {
const metrics = metricsRef.current;
metrics.renderCount++;
metrics.totalRenderTime += renderTime;
metrics.maxRenderedItems = Math.max(metrics.maxRenderedItems, renderedCount);
// Invio al sistema di monitoring
if (metrics.renderCount % 100 === 0) {
console.log("Virtual List Metrics:", {
avgRenderTime: metrics.totalRenderTime / metrics.renderCount,
maxRenderedItems: metrics.maxRenderedItems
});
}
}, []);
return { trackRender };
}Conclusione
La virtualizzazione delle ListView JavaScript non è un'ottimizzazione opzionale, ma una necessità per le applicazioni web moderne che visualizzano grandi quantità di dati. La combinazione di virtualizzazione frontend con TanStack Virtual, paginazione backend ottimizzata tramite NestJS e strategie di caching intelligenti consente esperienze utente fluide anche con centinaia di migliaia di elementi.
I punti chiave da ricordare:
- Mai renderizzare tutti gli elementi - La virtualizzazione è la chiave
- La paginazione basata su cursore si integra meglio con le liste virtuali rispetto a quella basata su offset
- Memoization e riferimenti stabili prevengono re-render non necessari
- Le altezze dinamiche richiedono una cache delle altezze con stime
- Il monitoring delle performance aiuta a identificare i problemi precocemente
Con queste tecniche è possibile sviluppare ListView che rimangono performanti anche con quantità estreme di dati, offrendo un'esperienza utente di prima classe.
Sapresti trovare il bug in Node.js / NestJS?
Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Scritto da
Anthony Fillion-MailletFondatore di SharpSkill
Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.
Aggiornato il 7 settembre 2026
Condividi
Articoli correlati

ListView JavaScript Performante nel 2026: Windowing, Virtualizzazione e Ottimizzazione delle Prestazioni
Scopri come creare ListView JavaScript ad alte prestazioni con windowing e virtualizzazione. TanStack Virtual, React Virtualized e tecniche avanzate per il 2026.

NestJS e Redis nel 2026: Caching, Sessioni e Domande per Colloqui Tecnici
Integrazione Redis in NestJS con @nestjs/cache-manager, gestione delle sessioni con connect-redis e domande frequenti nei colloqui per sviluppatori Node.js backend.

NestJS e MongoDB nel 2026: Mongoose, Aggregazioni e Domande da Colloquio
Guida pratica a NestJS 12 con MongoDB e Mongoose 9: design degli schema, pipeline di aggregazione, transazioni e domande tecniche per colloqui backend.