# Fast JavaScript ListView in 2026: Windowing, Virtualization and Performance Optimization > Master JavaScript list virtualization with TanStack Virtual, react-virtuoso and react-window. Learn windowing techniques to render 100,000+ items at 60fps with code examples. - Published: 2026-09-15 - Updated: 2026-09-15 - Author: Anthony Fillion-Maillet - Tags: javascript, performance, virtualization, react, typescript - Reading time: 9 min --- Rendering thousands of items in a JavaScript ListView without virtualization causes browsers to create thousands of DOM nodes, leading to slow initial render times, laggy scrolling, and memory consumption that can crash mobile devices. Fast JavaScript ListView implementations in 2026 solve this through windowing: rendering only the 10-20 items visible in the viewport while simulating a scrollable area containing all items. > **Windowing in One Sentence** > > Windowing renders only visible items plus a small buffer (overscan), reducing DOM nodes from 10,000 to roughly 20 regardless of list size. ## Why Browser Performance Degrades with Large Lists Every DOM node consumes memory and requires the browser to track its position during layout calculations. A list of 10,000 items with moderately complex markup (an avatar, two text spans, a button) creates 40,000+ DOM nodes. Chrome's rendering engine recalculates styles and positions for all of them on every scroll event. The result: initial render takes 2-4 seconds, scrolling drops to 15fps, and memory usage climbs to 500MB+. On mobile Safari, the tab often crashes outright. Virtualization fixes this by maintaining a constant DOM node count. Whether the data array contains 100 or 100,000 items, the rendered DOM stays at roughly 20-30 nodes, keeping memory flat and scroll performance at 60fps. ```typescript // VirtualizedList.tsx import { useVirtualizer } from '@tanstack/react-virtual' import { useRef } from 'react' interface Item { id: string name: string email: string } export function VirtualizedList({ items }: { items: Item[] }) { const parentRef = useRef(null) // Create virtualizer instance with estimated row height const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, // estimated height in pixels overscan: 5, // render 5 extra items above/below viewport }) return (
{virtualizer.getVirtualItems().map((virtualRow) => { const item = items[virtualRow.index] return (

{item.name}

{item.email}

) })}
) } ``` This implementation renders only visible rows plus the overscan buffer. The outer container sets the total scrollable height, making the scrollbar behave as if all items existed in the DOM. ## TanStack Virtual: The Headless Approach for 2026 [TanStack Virtual](https://tanstack.com/virtual/latest) provides framework-agnostic virtualization primitives. Unlike react-window or react-virtuoso, it delivers no pre-built components, only hooks that calculate which items to render and where to position them. This headless architecture means complete control over styling and DOM structure. The current version (3.13) supports React, Vue, Solid, Svelte, and vanilla JavaScript. Key configuration options: | Option | Purpose | Default | |--------|---------|--------| | `count` | Total number of items | required | | `estimateSize` | Function returning estimated item height/width | required | | `overscan` | Extra items rendered outside viewport | 1 | | `getScrollElement` | Returns the scrollable container | required | | `measureElement` | Custom measurement function for dynamic sizes | auto | The `overscan` value directly affects scroll smoothness. Setting it too low (0-2) causes white flashes during rapid scrolling. Setting it too high (20+) negates virtualization benefits. For most applications, 5-10 provides optimal balance. ```typescript // DynamicHeightList.tsx import { useVirtualizer } from '@tanstack/react-virtual' import { useRef, useCallback } from 'react' interface Message { id: string content: string timestamp: number } export function ChatList({ messages }: { messages: Message[] }) { const parentRef = useRef(null) // measureElement enables dynamic height measurement const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => parentRef.current, estimateSize: () => 80, // rough estimate, actual measured later overscan: 8, }) return (
{virtualizer.getVirtualItems().map((virtualRow) => { const message = messages[virtualRow.index] return (

{new Date(message.timestamp).toLocaleTimeString()}

{message.content}

) })}
) } ``` The `measureElement` ref enables automatic height measurement after render. This handles variable content lengths without requiring manual height calculations. ## react-virtuoso: Feature-Rich Alternative [react-virtuoso](https://virtuoso.dev/) takes the opposite approach: a batteries-included component library. Where TanStack Virtual requires building the scroll container and positioning logic, react-virtuoso handles these internally. The tradeoff is bundle size (37KB vs 12KB for TanStack Virtual) but significantly less boilerplate for common patterns like grouped lists, sticky headers, and chat interfaces with reverse scrolling. ```typescript // VirtuosoList.tsx import { Virtuoso } from 'react-virtuoso' interface User { id: string name: string department: string } export function UserDirectory({ users }: { users: User[] }) { return ( (
{user.name.charAt(0)}

{user.name}

{user.department}

)} /> ) } ``` This achieves the same virtualization result with less code. react-virtuoso automatically measures items, handles dynamic heights, and provides scroll position persistence. For [interview preparation on React performance](/technologies/react-next/interview-questions/styling-css-in-js), understanding when to choose each library demonstrates architectural awareness. TanStack Virtual suits applications already using TanStack Table or requiring framework-agnostic code. react-virtuoso fits teams prioritizing development speed over bundle size. ## Common Pitfalls and Solutions ### Scroll Position Jumps with Images Images load asynchronously, changing row height after initial measurement. The virtualizer cannot predict this change, causing content below to shift. The fix: reserve space for images using aspect-ratio or fixed dimensions. ```typescript // ImageListItem.tsx interface ImageItem { id: string src: string title: string } export function ImageListItem({ item }: { item: ImageItem }) { return (
{/* Reserve 80x80px space regardless of load state */}
{item.title}

{item.title}

) } ``` ### State Loss During Scroll Virtualized items unmount when scrolling out of view. Any local state (form inputs, expanded accordions) disappears. The solution: lift state to the parent component or a state management library, keyed by item ID. ```typescript // ExpandableList.tsx import { useState, useCallback } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' interface FAQ { id: string question: string answer: string } export function FAQList({ faqs }: { faqs: FAQ[] }) { // State lives in parent, survives row unmounting const [expandedIds, setExpandedIds] = useState>(new Set()) const parentRef = useRef(null) const toggleExpanded = useCallback((id: string) => { setExpandedIds(prev => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) }, []) const virtualizer = useVirtualizer({ count: faqs.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, overscan: 5, }) return (
{virtualizer.getVirtualItems().map((virtualRow) => { const faq = faqs[virtualRow.index] const isExpanded = expandedIds.has(faq.id) return (
) })}
) } ``` ### Unstable Keys Causing Re-renders Using array indices as keys works until the list reorders. Items receive wrong state, causing visual glitches and wasted renders. Always use stable, unique identifiers from the data itself: ```typescript // Correct: stable key from data {virtualizer.getVirtualItems().map((virtualRow) => { const item = items[virtualRow.index] return
...
// id from database/API })} // Wrong: index changes when list reorders {virtualizer.getVirtualItems().map((virtualRow, index) => { return
...
// causes bugs on reorder })} ``` ## Library Comparison for 2026 Projects Choosing the right virtualization library depends on project requirements. This comparison reflects the 2026 ecosystem: | Criteria | TanStack Virtual | react-virtuoso | react-window | |----------|-----------------|----------------|---------------| | Bundle size | 12KB | 37KB | 6KB | | Dynamic heights | Manual setup | Automatic | Not supported | | TypeScript | Native | Native | @types package | | Framework support | React, Vue, Solid, Svelte | React only | React only | | Maintenance | Active (2026) | Active (2026) | Maintenance mode | | Learning curve | Higher | Lower | Lowest | react-window remains viable for simple fixed-height lists where bundle size matters. However, its maintenance-mode status and lack of dynamic height support make it unsuitable for new projects with complex requirements. For [building performant React applications](/blog/react-next/react-testing-2026-vitest-rtl-best-practices), understanding virtualization tradeoffs demonstrates the performance awareness interviewers seek. ## Measuring Performance Improvements Quantifying virtualization benefits requires consistent measurement methodology. Chrome DevTools Performance panel provides the relevant metrics. Key measurements before/after virtualization: - **First Contentful Paint (FCP)**: Time until first content renders. Virtualization reduces this by limiting initial DOM creation. - **Total Blocking Time (TBT)**: Main thread blocking during load. Fewer DOM nodes mean less style/layout calculation. - **Memory usage**: Snapshot heap size in Memory panel. Should remain constant regardless of list length. - **Scroll fps**: Record a Performance trace while scrolling. Frame rate should stay at 60fps. ```typescript // PerformanceMonitor.tsx import { useEffect, useRef } from 'react' export function useScrollFPS(containerRef: React.RefObject) { const frameCountRef = useRef(0) const lastTimeRef = useRef(performance.now()) useEffect(() => { const container = containerRef.current if (!container) return let rafId: number const measureFrame = () => { frameCountRef.current++ const now = performance.now() if (now - lastTimeRef.current >= 1000) { console.log(`Scroll FPS: ${frameCountRef.current}`) frameCountRef.current = 0 lastTimeRef.current = now } rafId = requestAnimationFrame(measureFrame) } const handleScroll = () => { if (!rafId) { rafId = requestAnimationFrame(measureFrame) } } container.addEventListener('scroll', handleScroll, { passive: true }) return () => { container.removeEventListener('scroll', handleScroll) cancelAnimationFrame(rafId) } }, [containerRef]) } ``` This hook logs scroll FPS during development. Production applications should use observability tools like Sentry Performance or custom telemetry. ## Key Takeaways for JavaScript List Performance - Virtualization reduces DOM nodes from thousands to roughly 20, keeping memory and scroll performance constant regardless of data size - TanStack Virtual (12KB) provides headless primitives for maximum control across React, Vue, Solid, and Svelte - react-virtuoso (37KB) offers batteries-included components with automatic dynamic height handling - react-window remains in maintenance mode since 2022 and lacks dynamic height support - Set overscan between 5-10 for optimal scroll smoothness without negating virtualization benefits - Reserve image dimensions to prevent scroll position jumps during async loading - Lift expandable/editable state to the parent component since virtualized rows unmount during scroll - Use stable keys from data IDs, never array indices, to prevent state mismatches on reorder - Measure FCP, TBT, memory, and scroll FPS before/after implementation to quantify improvements --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/node-nestjs/fast-javascript-listview-2026-windowing-virtualization-performance