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.

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
// 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+ SpeicherVirtualisierung: 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.
// 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.
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
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
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:
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:
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:
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), // 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:
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:
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:
- Niemals alle Elemente rendern – Virtualisierung ist der Schlüssel
- Cursor-basierte Pagination harmoniert besser mit virtuellen Listen als Offset-basierte
- Memoization und stabile Referenzen verhindern unnötige Re-Renders
- Dynamische Höhen erfordern einen Höhen-Cache mit Schätzungen
- 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.
Findest du den Bug in Node.js / NestJS?
Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Geschrieben von
Anthony Fillion-MailletGrü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

Schnelle JavaScript ListView 2026: Windowing, Virtualisierung und Performance-Optimierung
Erfahren Sie, wie man mit Windowing und Virtualisierung performante JavaScript ListViews erstellt. React Virtualized, TanStack Virtual und weitere Techniken für 2026.

NestJS und Redis in 2026: Caching, Sessions und Technische Interviewfragen
NestJS Redis-Integration mit @nestjs/cache-manager, Session-Verwaltung mit connect-redis und häufige Interviewfragen für Node.js-Backend-Entwickler.

NestJS und MongoDB 2026: Mongoose-Integration, Aggregationen und Interview-Fragen
Praxisleitfaden für NestJS 12 mit MongoDB und Mongoose 9: Schema-Design, Aggregation-Pipelines, Transaktionen und technische Interview-Fragen für Backend-Entwickler.