High Performance JavaScript ListView 2026: Virtualisierung und Optimierungstechniken

Erfahren Sie, wie Sie mit JavaScript List-Virtualisierung und React Virtualized hochperformante ListViews entwickeln. Deep-Dive in Rendering-Optimierung für große Datenmengen.

High Performance JavaScript ListView 2026: Virtualisierung und Optimierungstechniken

In modernen Webanwendungen stellt die performante Darstellung großer Datenmengen eine der größten Herausforderungen dar. Ob es sich um E-Commerce-Produktkataloge, Social-Media-Feeds oder Daten-Dashboards handelt – die Fähigkeit, tausende Einträge flüssig zu rendern, entscheidet über Erfolg oder Misserfolg einer Anwendung. JavaScript ListView-Virtualisierung hat sich als Schlüsseltechnik etabliert, um diese Herausforderung zu meistern.

Die Virtualisierung von Listen kann den Speicherverbrauch um bis zu 95% reduzieren und die initiale Renderzeit von mehreren Sekunden auf unter 100ms senken – ein entscheidender Unterschied für die User Experience.

Das Problem: DOM-Überflutung bei großen Listen

Bevor die Lösungsansätze betrachtet werden, ist es wichtig, das grundlegende Problem zu verstehen. Wenn eine Anwendung versucht, 10.000 Listenelemente direkt in das DOM zu rendern, entstehen erhebliche Performance-Probleme:

  • Speicherverbrauch: Jedes DOM-Element benötigt Speicher für Styles, Event-Listener und Layout-Informationen
  • Rendering-Zeit: Der Browser muss jedes Element layouten und painten
  • Scrolling-Performance: Bei Scroll-Events muss der Browser ständig Reflows durchführen
typescript
// Naiver Ansatz - NICHT für große Listen verwenden
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>
  );
}

// Bei 10.000 Items: ~2-3 Sekunden Renderzeit, 500MB+ Speicher

Virtualisierung: Das Konzept der Fenster-Technik

Die Virtualisierung basiert auf einem einfachen, aber wirkungsvollen Prinzip: Es werden nur die Elemente gerendert, die aktuell im sichtbaren Bereich (Viewport) sind, plus ein kleiner Puffer darüber und darunter. Dies wird oft als "Windowing" oder "Fenster-Technik" bezeichnet.

typescript
// Grundprinzip der Virtualisierung
interface VirtualListConfig {
  totalItems: number;
  itemHeight: number;
  containerHeight: number;
  overscan: number; // Zusätzliche Items außerhalb des Viewports
}

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 };
}

Implementierung einer virtuellen Liste mit React

Die praktische Umsetzung erfordert mehrere Komponenten: einen Container, der das Scrolling verwaltet, und eine Rendering-Logik, die nur die sichtbaren Elemente darstellt.

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>
  );
}

Optimierung der Scroll-Performance

Die grundlegende Implementierung funktioniert, kann aber bei schnellem Scrollen zu Ruckeln führen. Mehrere Optimierungstechniken verbessern die Performance erheblich:

Passive Event Listener und 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 der Listenelemente

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;
});

// Verwendung mit stabilen Referenzen
function OptimizedVirtualList<T extends { id: string }>(props: VirtualListProps<T>) {
  const stableRenderItem = useMemo(
    () => props.renderItem,
    [props.renderItem]
  );
  
  // ... Rest der Implementierung
}

Dynamische Höhen: Eine fortgeschrittene Herausforderung

In der Realität haben Listenelemente oft unterschiedliche Höhen. Dies erfordert eine komplexere Implementierung mit einer Höhen-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 die geschätzte Höhe anpassen
    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
  };
}

Integration mit TanStack Virtual

Für Produktionsanwendungen empfiehlt sich die Verwendung etablierter Bibliotheken wie TanStack Virtual (ehemals React Virtual). Diese bieten ausgereiftere Implementierungen mit zusätzlichen Features:

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-Integration mit NestJS: Effiziente Pagination

Eine performante ListView erfordert auch ein optimiertes Backend. NestJS bietet elegante Lösungen für Cursor-basierte Pagination, die besser mit virtualisierten Listen harmoniert als Offset-basierte Ansätze:

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), // Maximal 100 Items
      direction: "forward"
    });
  }
}

Infinite Scrolling mit Virtualisierung kombinieren

Die Kombination von Infinite Scrolling mit Virtualisierung ermöglicht das Laden und Anzeigen praktisch unbegrenzter Datenmengen:

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
  });
  
  // Automatisches Nachladen
  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>
  );
}

Bereit für deine Node.js / NestJS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Performance-Metriken und Monitoring

Um die Effektivität der Virtualisierung zu messen, sollten relevante Metriken erfasst werden:

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);
    
    // An Monitoring-System senden
    if (metrics.renderCount % 100 === 0) {
      console.log("Virtual List Metrics:", {
        avgRenderTime: metrics.totalRenderTime / metrics.renderCount,
        maxRenderedItems: metrics.maxRenderedItems
      });
    }
  }, []);
  
  return { trackRender };
}

Fazit

Die Virtualisierung von JavaScript ListViews ist keine optionale Optimierung, sondern eine Notwendigkeit für moderne Webanwendungen, die große Datenmengen darstellen. Die Kombination aus Frontend-Virtualisierung mit TanStack Virtual, optimierter Backend-Pagination via NestJS und intelligenten Caching-Strategien ermöglicht flüssige User Experiences auch bei hunderttausenden von Einträgen.

Die wichtigsten Erkenntnisse zusammengefasst:

  1. Niemals alle Elemente rendern – Virtualisierung ist der Schlüssel
  2. Cursor-basierte Pagination harmoniert besser mit virtuellen Listen als Offset-basierte
  3. Memoization und stabile Referenzen verhindern unnötige Re-Renders
  4. Dynamische Höhen erfordern einen Höhen-Cache mit Schätzungen
  5. Performance-Monitoring hilft, Probleme frühzeitig zu erkennen

Mit diesen Techniken lassen sich ListViews entwickeln, die selbst bei extremen Datenmengen performant bleiben und eine erstklassige User Experience bieten.

Tägliche Challenge

Findest du den Bug in Node.js / NestJS?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 7. September 2026

Teilen

Verwandte Artikel