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.

High Performance JavaScript ListView in 2026: Virtualisatie en Optimalisatietechnieken

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
typescript
// 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+ geheugen

Virtualisatie: 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.

typescript
// 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.

typescript
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

typescript
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

typescript
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:

typescript
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:

typescript
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:

items.service.tstypescript
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
    };
  }
}
items.controller.tstypescript
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:

typescript
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:

typescript
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:

  1. Nooit alle elementen renderen - Virtualisatie is de sleutel
  2. Cursor-gebaseerde paginatie harmonieert beter met virtuele lijsten dan offset-gebaseerde
  3. Memoization en stabiele referenties voorkomen onnodige re-renders
  4. Dynamische hoogtes vereisen een hoogte-cache met schattingen
  5. 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.

Dagelijkse challenge

Zie jij de bug in Node.js / NestJS?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter 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