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.

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 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.
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<HTMLDivElement>(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 (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = items[virtualRow.index]
return (
<div
key={item.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div className="flex items-center gap-3 p-3 border-b">
<div className="w-10 h-10 rounded-full bg-gray-200" />
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-gray-500">{item.email}</p>
</div>
</div>
</div>
)
})}
</div>
</div>
)
}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 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.
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<HTMLDivElement>(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 (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const message = messages[virtualRow.index]
return (
<div
key={message.id}
data-index={virtualRow.index}
ref={virtualizer.measureElement} // enables auto-measurement
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div className="p-4 border-b">
<p className="text-sm text-gray-500">
{new Date(message.timestamp).toLocaleTimeString()}
</p>
<p className="mt-1">{message.content}</p>
</div>
</div>
)
})}
</div>
</div>
)
}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 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.
import { Virtuoso } from 'react-virtuoso'
interface User {
id: string
name: string
department: string
}
export function UserDirectory({ users }: { users: User[] }) {
return (
<Virtuoso
style={{ height: '600px' }}
data={users}
itemContent={(index, user) => (
<div className="flex items-center gap-3 p-4 border-b hover:bg-gray-50">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
{user.name.charAt(0)}
</div>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.department}</p>
</div>
</div>
)}
/>
)
}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, 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.
Ready to ace your Node.js / NestJS interviews?
Practice with our interactive simulators, flashcards, and technical tests.
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.
interface ImageItem {
id: string
src: string
title: string
}
export function ImageListItem({ item }: { item: ImageItem }) {
return (
<div className="flex gap-4 p-4 border-b">
{/* Reserve 80x80px space regardless of load state */}
<div className="w-20 h-20 flex-shrink-0 bg-gray-100">
<img
src={item.src}
alt={item.title}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
<p className="font-medium">{item.title}</p>
</div>
)
}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.
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<Set<string>>(new Set())
const parentRef = useRef<HTMLDivElement>(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 (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const faq = faqs[virtualRow.index]
const isExpanded = expandedIds.has(faq.id)
return (
<div
key={faq.id}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<button
onClick={() => toggleExpanded(faq.id)}
className="w-full text-left p-4 border-b"
>
<p className="font-medium">{faq.question}</p>
{isExpanded && (
<p className="mt-2 text-gray-600">{faq.answer}</p>
)}
</button>
</div>
)
})}
</div>
</div>
)
}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:
// Correct: stable key from data
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = items[virtualRow.index]
return <div key={item.id}>...</div> // id from database/API
})}
// Wrong: index changes when list reorders
{virtualizer.getVirtualItems().map((virtualRow, index) => {
return <div key={index}>...</div> // 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, 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.
import { useEffect, useRef } from 'react'
export function useScrollFPS(containerRef: React.RefObject<HTMLElement>) {
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.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
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
Can you spot the bug in Node.js / NestJS?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 15, 2026
Tags
Share
Related articles

High Performance JavaScript ListView in 2026: Virtualization and Optimization Techniques
Master JavaScript list virtualization with TanStack Virtual, react-virtuoso, and react-window. Learn windowing techniques, dynamic sizing, and performance optimizations for rendering 100k+ items smoothly.

NestJS + Prisma: The Modern Backend Stack for Node.js
Complete guide to building a modern backend API with NestJS and Prisma. Setup, models, services, transactions and best practices explained.

Node.js Backend Interview Questions: Complete Guide 2026
The 25 most common Node.js backend interview questions. Event loop, async/await, streams, clustering and performance explained with detailed answers.