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

Високопродуктивний рендеринг JavaScript ListView стає критично важливим, коли застосунки мають відображати тисячі елементів без зависання браузера. Віртуалізація списків, техніка рендерингу лише видимих елементів з повторним використанням DOM-вузлів, перетворює повільне прокручування на плавну взаємодію.
Віртуалізація рендерить лише елементи, видимі у viewport, плюс невеликий буфер overscan. Список із 100 000 елементів з 10 видимими рядками створює лише 15-20 DOM-вузлів замість 100 000, зменшуючи використання пам'яті з сотень мегабайтів до кілобайтів.
Чому кількість DOM-вузлів знищує продуктивність прокручування
Браузери мають проблеми з великими DOM-деревами. Кожен вузол потребує пам'яті для самого елемента, його обчислених стилів та даних макета. Понад 5000 вузлів reflow стають помітними. При 50 000 вузлах події прокручування можуть займати понад 100мс, спричиняючи видимі затримки.
Вартість продуктивності зростає нелінійно. Pipeline рендерингу Chrome повинен:
- Виконати обробники подій JavaScript
- Перерахувати стилі для зачеплених елементів
- Обчислити позиції макета
- Намалювати пікселі на шарах
- Скомпонувати шари для фінального відображення
Віртуалізація повністю усуває кроки 2-4 для позаекранних елементів. Браузер підтримує фіксований розмір DOM незалежно від довжини даних.
TanStack Virtual: Стандарт безголової віртуалізації
TanStack Virtual надає framework-незалежні примітиви віртуалізації. Версія 3.13 (випущена в серпні 2026) принесла значні покращення продуктивності: холодний mount для 100 000 елементів знизився з 6,1мс до 4,5мс, а найгірший сценарій resize покращився з 2 секунд до 1,3 мілісекунди.
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
interface VirtualListProps<T> {
items: T[]
estimateSize: number
renderItem: (item: T, index: number) => React.ReactNode
}
export function VirtualList<T>({ items, estimateSize, renderItem }: VirtualListProps<T>) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan: 5, // Рендерити 5 додаткових елементів вище/нижче viewport
})
return (
<div ref={parentRef} style={{ height: '100%', 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)`,
}}
>
{renderItem(items[virtualItem.index], virtualItem.index)}
</div>
))}
</div>
</div>
)
}TanStack Virtual використовує transform: translateY() замість позиціонування top. Це запускає GPU-композицію замість перерахунку макета, утримуючи прокручування на 60fps навіть при складному рендерингу елементів.
Динамічні висоти елементів з автоматичним вимірюванням
Елементи фіксованої висоти спрощують математику віртуалізації, але реальні застосунки часто потребують змінних висот. TanStack Virtual вимірює елементи автоматично за допомогою ResizeObserver:
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef, useCallback } from 'react'
interface Message {
id: string
text: string
timestamp: number
}
export function ChatVirtualList({ messages }: { messages: Message[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: useCallback(() => 72, []), // Початкова оцінка
measureElement: (element) => element.getBoundingClientRect().height,
})
return (
<div ref={parentRef} className="h-full overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ChatBubble message={messages[virtualItem.index]} />
</div>
))}
</div>
</div>
)
}Callback measureElement отримує кожен відрендерений елемент і повертає його виміряну висоту. TanStack Virtual кешує ці вимірювання і повторно вимірює лише коли ResizeObserver виявляє зміни.
React Virtuoso: Готовий до продакшену одразу
React Virtuoso (версія 4.18, вересень 2026) пропонує API вищого рівня, яке автоматично обробляє крайні випадки. З 2,8 мільйона щотижневих завантажень на npm, він став вибором за замовчуванням для команд, які хочуть мінімальну конфігурацію.
import { Virtuoso } from 'react-virtuoso'
interface Product {
id: string
name: string
price: number
description: string
}
export function ProductList({ products }: { products: Product[] }) {
return (
<Virtuoso
data={products}
itemContent={(index, product) => (
<div className="p-4 border-b border-gray-200">
<h3 className="font-semibold">{product.name}</h3>
<p className="text-sm text-gray-600">{product.description}</p>
<span className="text-lg font-bold">${product.price}</span>
</div>
)}
style={{ height: '100%' }}
/>
)
}Virtuoso обробляє змінні висоти, згруповані елементи та зворотне прокручування без додаткової конфігурації. Для чат-застосунків або переглядачів логів, які потребують додавання елементів на початок зі збереженням позиції прокручування, це працює безперебійно:
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
import { useRef, useState, useCallback } from 'react'
export function ChatWithPrepend() {
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [messages, setMessages] = useState<Message[]>(initialMessages)
const prependMessages = useCallback((newMessages: Message[]) => {
setMessages((prev) => [...newMessages, ...prev])
}, [])
return (
<Virtuoso
ref={virtuosoRef}
data={messages}
firstItemIndex={10000 - messages.length} // Зсув віртуального індексу
initialTopMostItemIndex={messages.length - 1}
followOutput="smooth" // Автопрокручування до нових повідомлень
startReached={() => loadOlderMessages().then(prependMessages)}
itemContent={(index, message) => <ChatBubble message={message} />}
/>
)
}Властивість firstItemIndex дозволяє двонаправлене нескінченне прокручування. Virtuoso підтримує позицію прокручування при додаванні на початок, коригуючи віртуальні індекси замість зсуву позицій DOM.
Готовий до співбесід з Node.js / NestJS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Порівняння продуктивності: TanStack vs Virtuoso vs react-window
Бенчмарки на ноутбуці середнього класу 2026 року (M3 MacBook Air) зі 100 000 елементів:
| Бібліотека | Початковий mount | Прокрутка до середини | Використання пам'яті |
|---|---|---|---|
| TanStack Virtual 3.13 | 4,5мс | 12мс | 2,1МБ |
| react-virtuoso 4.18 | 6,2мс | 15мс | 3,4МБ |
| react-window 1.8 | 3,8мс | 8мс | 1,8МБ |
React-window залишається найшвидшим для елементів фіксованої висоти, але йому бракує динамічного вимірювання. TanStack Virtual пропонує найкращий баланс між продуктивністю та функціями. Virtuoso пріоритизує досвід розробника з автоматичною обробкою крайніх випадків.
Оптимізація рендерингу елементів через мемоізацію
Віртуалізація зменшує DOM-вузли, але не може запобігти дорогим повторним рендерам всередині елементів. React.memo стає необхідним:
import { memo } from 'react'
interface ListItemProps {
item: Product
onSelect: (id: string) => void
}
export const ListItem = memo(function ListItem({ item, onSelect }: ListItemProps) {
return (
<div
className="p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => onSelect(item.id)}
>
<h3>{item.name}</h3>
<p>{item.description}</p>
</div>
)
}, (prevProps, nextProps) => {
// Власне порівняння: рендерити повторно лише коли дані елемента змінюються
return prevProps.item.id === nextProps.item.id &&
prevProps.item.name === nextProps.item.name
})Функція власного порівняння запобігає повторним рендерам, коли змінюється лише посилання, але вміст залишається ідентичним, що часто трапляється при патернах отримання даних, які повертають нові посилання на об'єкти.
Обробка відновлення позиції прокручування
Користувачі очікують збереження позиції прокручування при переході туди й назад. History API у поєднанні зі станом virtualizer дозволяє це:
import { useEffect, useRef } from 'react'
import { Virtualizer } from '@tanstack/react-virtual'
export function useScrollRestoration(
virtualizer: Virtualizer<HTMLDivElement, Element>,
storageKey: string
) {
const restoredRef = useRef(false)
// Відновити при монтуванні
useEffect(() => {
if (restoredRef.current) return
const saved = sessionStorage.getItem(storageKey)
if (saved) {
const { offset } = JSON.parse(saved)
virtualizer.scrollToOffset(offset, { align: 'start' })
}
restoredRef.current = true
}, [virtualizer, storageKey])
// Зберегти перед демонтуванням
useEffect(() => {
return () => {
const offset = virtualizer.scrollOffset
sessionStorage.setItem(storageKey, JSON.stringify({ offset }))
}
}, [virtualizer, storageKey])
}Цей патерн зберігає зсув прокручування в sessionStorage, який переживає навігацію між сторінками, але очищається при закритті вкладки.
Теми співбесід: Що рекрутери запитують про віртуалізацію списків
Технічні співбесіди дедалі частіше охоплюють знання про віртуалізацію. Типові запитання та сильні відповіді:
П: Чому браузери не можуть ефективно обробляти 100k DOM-вузлів? Pipeline рендерингу обробляє кожен вузол під час обчислення стилів та макета. Тиск пам'яті від об'єктів вузлів, обчислених стилів та дерев макета накопичується. Понад 10k вузлів паузи garbage collection стають помітними під час прокручування.
П: Поясніть windowing vs recycling. Windowing рендерить лише видимі елементи. Recycling повторно використовує існуючі DOM-вузли для нових даних, коли елементи прокручуються за межі видимості. TanStack Virtual та react-virtuoso використовують windowing. Нативні мобільні фреймворки (iOS UITableView, Android RecyclerView) використовують recycling.
П: Як би ви віртуалізували сітку зображень?
Використовуйте useVirtualizer з TanStack Virtual з вимірами як рядків, так і стовпців. Реалізуйте estimateSize для обох осей. Додайте lazy loading зображень з IntersectionObserver. Розгляньте CSS content-visibility: auto для елементів біля краю viewport.
Більше підготовки до співбесід на теми продуктивності фронтенду можна знайти в модулі питань до співбесіди про тестування React, який охоплює пов'язані патерни оптимізації.
Поширені пастки та як їх уникнути
Неправильна висота контейнера: Віртуалізація вимагає контейнера прокручування фіксованої висоти. Використання height: auto або покладання на висоту вмісту псує обчислення вимірювань.
Відсутні ключі: React потребує стабільних ключів для елементів, що підлягають повторному використанню. Використання індексів масиву спричиняє помилки стану при зміні порядку елементів.
Занадто низький overscan: Нульовий overscan спричиняє видимі порожні області під час швидкого прокручування. Значення між 3-10 балансують продуктивність та візуальну плавність.
Немемоізовані callback'и: Передача inline-функцій як пропсів елементів викликає повторні рендери при кожному прокручуванні. Виділіть обробники в useCallback або область компонента.
Ключові висновки щодо віртуалізації списків JavaScript
- Кількість DOM-вузлів, а не розмір даних, визначає продуктивність прокручування. Віртуалізація обмежує вузли незалежно від довжини списку.
- TanStack Virtual 3.13 підходить для застосунків, що потребують гнучкості фреймворку та максимального контролю. Продуктивність холодного монтування покращилася на 35% порівняно з попередніми версіями.
- react-virtuoso 4.18 обробляє динамічні висоти, групування та двонаправлене прокручування з мінімальним кодом. 2,8М щотижневих завантажень відображає стабільність продакшену.
- react-window залишається життєздатним для сценаріїв фіксованої висоти, де важливий розмір бандла, хоча активна розробка припинилася.
- Мемоізуйте елементи списку з функціями власного порівняння. Перевірка рівності посилань не спрацьовує, коли джерела даних створюють нові об'єкти.
- Зберігайте зсув прокручування в sessionStorage для відновлення між навігаціями. Зсув virtualizer безпосередньо відображається на позицію пікселя.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Чи знайдеш ти помилку в Node.js / NestJS?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

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

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

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

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