High Performance JavaScript ListView in 2026: Virtualisatie en Optimalisatietechnieken
Ontdek hoe je met JavaScript list virtualization en React Virtualized hoogperformante ListViews ontwikkelt. Deep-dive in rendering optimalisatie voor grote datasets.

In moderne webapplicaties vormt het performant weergeven van grote hoeveelheden data een van de grootste uitdagingen. Of het nu gaat om e-commerce productcatalogi, social media feeds of data dashboards - het vermogen om duizenden items vloeiend te renderen bepaalt het succes of falen van een applicatie. JavaScript ListView virtualisatie heeft zich bewezen als de sleuteltechniek om deze uitdaging aan te gaan.
Lijst virtualisatie kan het geheugenverbruik met tot 95% verminderen en de initiële rendertijd terugbrengen van meerdere seconden naar minder dan 100ms - een cruciaal verschil voor de gebruikerservaring.
Het Probleem: DOM-overbelasting bij Grote Lijsten
Voordat de oplossingsbenaderingen worden besproken, is het belangrijk om het fundamentele probleem te begrijpen. Wanneer een applicatie probeert om 10.000 lijstelementen direct in de DOM te renderen, ontstaan er aanzienlijke performance problemen:
- Geheugenverbruik: Elk DOM-element vereist geheugen voor styles, event listeners en layout-informatie
- Rendertijd: De browser moet elk element layouten en painten
- Scroll performance: Bij scroll events moet de browser continu reflows uitvoeren
// Naïeve benadering - NIET gebruiken voor grote lijsten
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>
);
}
// Bij 10.000 items: ~2-3 seconden rendertijd, 500MB+ geheugenVirtualisatie: Het Concept van de Window-techniek
Virtualisatie is gebaseerd op een eenvoudig maar effectief principe: alleen de elementen worden gerenderd die momenteel zichtbaar zijn in de viewport, plus een kleine buffer erboven en eronder. Dit wordt vaak "windowing" of de "window-techniek" genoemd.
// Basisprincipe van virtualisatie
interface VirtualListConfig {
totalItems: number;
itemHeight: number;
containerHeight: number;
overscan: number; // Extra items buiten de 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 };
}Implementatie van een Virtuele Lijst met React
De praktische implementatie vereist meerdere componenten: een container die het scrollen beheert en een rendering-logica die alleen de zichtbare elementen weergeeft.
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>
);
}Optimalisatie van Scroll Performance
De basis implementatie werkt, maar kan bij snel scrollen haperen. Meerdere optimalisatietechnieken verbeteren de performance aanzienlijk:
Passive Event Listeners en 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 van Lijstelementen
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;
});
// Gebruik met stabiele referenties
function OptimizedVirtualList<T extends { id: string }>(props: VirtualListProps<T>) {
const stableRenderItem = useMemo(
() => props.renderItem,
[props.renderItem]
);
// ... rest van de implementatie
}Dynamische Hoogtes: Een Geavanceerde Uitdaging
In de praktijk hebben lijstelementen vaak verschillende hoogtes. Dit vereist een complexere implementatie met een hoogte-cache:
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);
// Dynamisch de geschatte hoogte aanpassen
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
};
}Integratie met TanStack Virtual
Voor productie-applicaties wordt het gebruik van gevestigde bibliotheken zoals TanStack Virtual (voorheen React Virtual) aanbevolen. Deze bieden meer volwassen implementaties met extra functionaliteiten:
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>
);
}Backend Integratie met NestJS: Efficiënte Paginatie
Een performante ListView vereist ook een geoptimaliseerde backend. NestJS biedt elegante oplossingen voor cursor-gebaseerde paginatie, die beter harmonieert met gevirtualiseerde lijsten dan offset-gebaseerde benaderingen:
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), // Maximaal 100 items
direction: "forward"
});
}
}Infinite Scrolling Combineren met Virtualisatie
De combinatie van infinite scrolling met virtualisatie maakt het laden en weergeven van praktisch onbeperkte hoeveelheden data mogelijk:
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
});
// Automatisch herladen
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>
);
}Klaar om je Node.js / NestJS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Performance Metrieken en Monitoring
Om de effectiviteit van virtualisatie te meten, moeten relevante metrieken worden verzameld:
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);
// Verstuur naar monitoring systeem
if (metrics.renderCount % 100 === 0) {
console.log("Virtual List Metrics:", {
avgRenderTime: metrics.totalRenderTime / metrics.renderCount,
maxRenderedItems: metrics.maxRenderedItems
});
}
}, []);
return { trackRender };
}Conclusie
Virtualisatie van JavaScript ListViews is geen optionele optimalisatie, maar een noodzaak voor moderne webapplicaties die grote hoeveelheden data weergeven. De combinatie van frontend virtualisatie met TanStack Virtual, geoptimaliseerde backend paginatie via NestJS en intelligente caching strategieën maakt vloeiende gebruikerservaringen mogelijk, zelfs bij honderdduizenden items.
De belangrijkste inzichten samengevat:
- Nooit alle elementen renderen - Virtualisatie is de sleutel
- Cursor-gebaseerde paginatie harmonieert beter met virtuele lijsten dan offset-gebaseerde
- Memoization en stabiele referenties voorkomen onnodige re-renders
- Dynamische hoogtes vereisen een hoogte-cache met schattingen
- Performance monitoring helpt problemen vroegtijdig te identificeren
Met deze technieken kunnen ListViews worden ontwikkeld die zelfs bij extreme hoeveelheden data performant blijven en een eersteklas gebruikerservaring bieden.
Zie jij de bug in Node.js / NestJS?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 7 september 2026
Delen
Gerelateerde artikelen

Snelle JavaScript ListView in 2026: Windowing, Virtualisatie en Performance-optimalisatie
Leer hoe je met windowing en virtualisatie performante JavaScript ListViews bouwt. TanStack Virtual, React Virtualized en geavanceerde technieken voor 2026.

NestJS en Redis in 2026: Caching, Sessies en Technische Sollicitatievragen
NestJS Redis-integratie met @nestjs/cache-manager, sessiebeheer met connect-redis en veelvoorkomende sollicitatievragen voor Node.js backend-ontwikkelaars.

NestJS en MongoDB in 2026: Mongoose, Aggregaties en Sollicitatievragen
Praktische handleiding voor NestJS 12 met MongoDB en Mongoose 9: schema-ontwerp, aggregation pipelines, transacties en technische sollicitatievragen voor backend-ontwikkelaars.