Швидкий JavaScript ListView у 2026: Windowing, Віртуалізація та Оптимізація Продуктивності
Опануйте техніки windowing та віртуалізації для рендерингу тисяч елементів у JavaScript. TanStack Virtual, react-virtuoso та найкращі практики для ефективних списків у 2026 році.

Рендеринг тисяч елементів у JavaScript ListView без віртуалізації змушує браузер створювати тисячі DOM-вузлів, що призводить до повільного початкового рендерингу, лагів при прокручуванні та споживання пам'яті, яке може спричинити збій мобільних пристроїв. Швидкі реалізації JavaScript ListView у 2026 році вирішують цю проблему через windowing: рендеринг лише 10-20 елементів, видимих у viewport, одночасно симулюючи область прокрутки, що містить усі елементи.
Windowing рендерить лише видимі елементи плюс невеликий буфер (overscan), зменшуючи кількість DOM-вузлів з 10 000 до приблизно 20, незалежно від розміру списку.
Чому продуктивність браузера падає при великих списках
Кожен DOM-вузол споживає пам'ять і вимагає від браузера відстежувати його позицію під час обчислень макету. Список із 10 000 елементів з помірно складною розміткою (аватар, два текстові спани, кнопка) створює понад 40 000 DOM-вузлів. Рендеринговий рушій Chrome перераховує стилі та позиції для всіх них при кожній події прокрутки.
Результат: початковий рендеринг займає 2-4 секунди, прокрутка падає до 15fps, а використання пам'яті зростає до понад 500MB. На мобільному Safari вкладка часто повністю падає.
Віртуалізація виправляє це, підтримуючи постійну кількість DOM-вузлів. Незалежно від того, чи масив даних містить 100 чи 100 000 елементів, рендерений DOM залишається на рівні приблизно 20-30 вузлів, зберігаючи плоске споживання пам'яті та продуктивність прокрутки на рівні 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>
)
}Ця реалізація рендерить лише видимі рядки плюс буфер overscan. Зовнішній контейнер встановлює загальну висоту прокрутки, змушуючи смугу прокрутки поводитися так, ніби всі елементи існують у DOM.
TanStack Virtual: Безголова архітектура для 2026 року
TanStack Virtual надає framework-агностичні примітиви віртуалізації. На відміну від react-window чи react-virtuoso, не пропонує готових компонентів - лише хуки, що обчислюють які елементи рендерити і де їх позиціонувати. Ця безголова архітектура означає повний контроль над стилями та структурою DOM.
Поточна версія (3.13) підтримує React, Vue, Solid, Svelte та чистий JavaScript. Ключові параметри конфігурації:
| Параметр | Призначення | Значення за замовчуванням |
|---|---|---|
count | Загальна кількість елементів | обов'язковий |
estimateSize | Функція, що повертає орієнтовну висоту/ширину елемента | обов'язковий |
overscan | Додаткові елементи, що рендеряться за межами viewport | 1 |
getScrollElement | Повертає контейнер прокрутки | обов'язковий |
measureElement | Власна функція вимірювання для динамічних розмірів | авто |
Значення overscan безпосередньо впливає на плавність прокрутки. Занадто низьке значення (0-2) спричиняє білі спалахи під час швидкої прокрутки. Занадто високе (20+) нівелює переваги віртуалізації. Для більшості застосунків 5-10 забезпечує оптимальний баланс.
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>
)
}Ref measureElement дозволяє автоматичне вимірювання висоти після рендерингу. Це обробляє змінну довжину контенту без необхідності ручних обчислень висоти.
react-virtuoso: Багатофункціональна альтернатива
react-virtuoso застосовує протилежний підхід: бібліотека компонентів з усім необхідним. Там, де TanStack Virtual вимагає побудови контейнера прокрутки та логіки позиціонування, react-virtuoso обробляє це внутрішньо.
Компроміс — розмір пакету (37KB проти 12KB для TanStack Virtual), але значно менше шаблонного коду для типових патернів, як-от групові списки, липкі заголовки та інтерфейси чату зі зворотною прокруткою.
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>
)}
/>
)
}Це досягає того ж результату віртуалізації з меншою кількістю коду. react-virtuoso автоматично вимірює елементи, обробляє динамічні висоти та забезпечує збереження позиції прокрутки.
Під час підготовки до співбесіди з продуктивності React, розуміння коли обирати кожну бібліотеку демонструє архітектурну обізнаність. TanStack Virtual підходить для застосунків, що вже використовують TanStack Table або потребують framework-агностичний код. react-virtuoso підходить командам, що пріоритезують швидкість розробки над розміром пакету.
Готовий до співбесід з Node.js / NestJS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Типові пастки та рішення
Стрибки позиції прокрутки при зображеннях
Зображення завантажуються асинхронно, змінюючи висоту рядка після початкового вимірювання. Віртуалізатор не може передбачити цю зміну, спричиняючи зсув контенту нижче.
Рішення: резервування місця для зображень за допомогою aspect-ratio або фіксованих розмірів.
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>
)
}Втрата стану під час прокрутки
Віртуалізовані елементи розмонтовуються при прокручуванні за межі видимої області. Будь-який локальний стан (поля форм, розгорнуті акордеони) зникає.
Рішення: перенесення стану до батьківського компонента або бібліотеки управління станом з ключем за ID елемента.
import { useState, useCallback, useRef } 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>
)
}Нестабільні ключі, що спричиняють повторні рендери
Використання індексів масиву як ключів працює, поки список не буде пересортовано. Елементи отримують неправильний стан, спричиняючи візуальні помилки та марні рендери.
Завжди слід використовувати стабільні, унікальні ідентифікатори з самих даних:
// 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
})}Порівняння бібліотек для проектів 2026 року
Вибір правильної бібліотеки віртуалізації залежить від вимог проекту. Це порівняння відображає екосистему 2026 року:
| Критерій | TanStack Virtual | react-virtuoso | react-window |
|---|---|---|---|
| Розмір пакету | 12KB | 37KB | 6KB |
| Динамічні висоти | Ручне налаштування | Автоматично | Не підтримується |
| TypeScript | Нативний | Нативний | Пакет @types |
| Підтримка фреймворків | React, Vue, Solid, Svelte | Тільки React | Тільки React |
| Підтримка | Активна (2026) | Активна (2026) | Режим підтримки |
| Крива навчання | Вища | Нижча | Найнижча |
react-window залишається варіантом для простих списків з фіксованою висотою, де важливий розмір пакету. Однак його статус режиму підтримки та відсутність підтримки динамічних висот робить його непридатним для нових проектів зі складними вимогами.
Для побудови продуктивних React-застосунків, розуміння компромісів віртуалізації демонструє усвідомлення продуктивності, яке шукають інтерв'юери.
Вимірювання покращень продуктивності
Кількісна оцінка переваг віртуалізації вимагає послідовної методології вимірювання. Панель Performance у Chrome DevTools надає відповідні метрики.
Ключові вимірювання до/після віртуалізації:
- First Contentful Paint (FCP): Час до першого рендерингу контенту. Віртуалізація зменшує його, обмежуючи початкове створення DOM.
- Total Blocking Time (TBT): Блокування основного потоку під час завантаження. Менше DOM-вузлів означає менше обчислень стилю/макету.
- Використання пам'яті: Знімок розміру купи в панелі Memory. Має залишатися постійним незалежно від довжини списку.
- FPS прокрутки: Запис Performance trace під час прокрутки. Частота кадрів має залишатися на рівні 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])
}Цей хук логує FPS прокрутки під час розробки. Продакшн-застосунки повинні використовувати інструменти спостережуваності, як-от Sentry Performance або власну телеметрію.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Ключові висновки для продуктивності списків JavaScript
- Віртуалізація зменшує DOM-вузли з тисяч до приблизно 20, зберігаючи постійну пам'ять та продуктивність прокрутки незалежно від розміру даних
- TanStack Virtual (12KB) надає безголові примітиви для максимального контролю в React, Vue, Solid та Svelte
- react-virtuoso (37KB) пропонує готові компоненти з автоматичною обробкою динамічних висот
- react-window залишається в режимі підтримки з 2022 року і не підтримує динамічні висоти
- Встановлення overscan між 5-10 забезпечує оптимальну плавність прокрутки без нівелювання переваг віртуалізації
- Резервування розмірів зображень запобігає стрибкам позиції прокрутки під час асинхронного завантаження
- Перенесення розгортаних/редагованих станів до батьківського компонента, оскільки віртуалізовані рядки розмонтовуються під час прокрутки
- Використання стабільних ключів з ID даних, ніколи індексів масиву, для запобігання невідповідностям стану при пересортуванні
- Вимірювання FCP, TBT, пам'яті та FPS прокрутки до/після реалізації для кількісної оцінки покращень
Чи знайдеш ти помилку в Node.js / NestJS?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 15 вересня 2026 р.
Поділитися
Пов'язані статті

NestJS та Redis у 2026: Кешування, Сесії та Питання на Співбесідах
Повний посібник з інтеграції Redis у NestJS. Cache-manager, управління сесіями та стратегії інвалідації кешу.

Високопродуктивний JavaScript ListView у 2026: Техніки віртуалізації та оптимізації
Опануйте віртуалізацію списків JavaScript з TanStack Virtual, react-virtuoso та react-window. Рендеріть понад 100 тисяч елементів плавно завдяки технікам windowing.

NestJS та MongoDB у 2026: Mongoose, Агрегації та Питання на Співбесідах
Опануйте NestJS з MongoDB та Mongoose 9. Вивчіть патерни проектування схем, конвеєри агрегацій та підготуйтесь до технічних співбесід з практичними прикладами.