Високопродуктивний JavaScript ListView у 2026: Техніки віртуалізації та оптимізації

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

Високопродуктивний JavaScript ListView у 2026: Техніки віртуалізації та оптимізації

Високопродуктивний рендеринг JavaScript ListView стає критично важливим, коли застосунки мають відображати тисячі елементів без зависання браузера. Віртуалізація списків, техніка рендерингу лише видимих елементів з повторним використанням DOM-вузлів, перетворює повільне прокручування на плавну взаємодію.

Основний принцип віртуалізації

Віртуалізація рендерить лише елементи, видимі у viewport, плюс невеликий буфер overscan. Список із 100 000 елементів з 10 видимими рядками створює лише 15-20 DOM-вузлів замість 100 000, зменшуючи використання пам'яті з сотень мегабайтів до кілобайтів.

Чому кількість DOM-вузлів знищує продуктивність прокручування

Браузери мають проблеми з великими DOM-деревами. Кожен вузол потребує пам'яті для самого елемента, його обчислених стилів та даних макета. Понад 5000 вузлів reflow стають помітними. При 50 000 вузлах події прокручування можуть займати понад 100мс, спричиняючи видимі затримки.

Вартість продуктивності зростає нелінійно. Pipeline рендерингу Chrome повинен:

  1. Виконати обробники подій JavaScript
  2. Перерахувати стилі для зачеплених елементів
  3. Обчислити позиції макета
  4. Намалювати пікселі на шарах
  5. Скомпонувати шари для фінального відображення

Віртуалізація повністю усуває кроки 2-4 для позаекранних елементів. Браузер підтримує фіксований розмір DOM незалежно від довжини даних.

TanStack Virtual: Стандарт безголової віртуалізації

TanStack Virtual надає framework-незалежні примітиви віртуалізації. Версія 3.13 (випущена в серпні 2026) принесла значні покращення продуктивності: холодний mount для 100 000 елементів знизився з 6,1мс до 4,5мс, а найгірший сценарій resize покращився з 2 секунд до 1,3 мілісекунди.

VirtualList.tsxtypescript
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:

DynamicVirtualList.tsxtypescript
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, він став вибором за замовчуванням для команд, які хочуть мінімальну конфігурацію.

VirtuosoList.tsxtypescript
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 обробляє змінні висоти, згруповані елементи та зворотне прокручування без додаткової конфігурації. Для чат-застосунків або переглядачів логів, які потребують додавання елементів на початок зі збереженням позиції прокручування, це працює безперебійно:

ChatWithPrepend.tsxtypescript
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.134,5мс12мс2,1МБ
react-virtuoso 4.186,2мс15мс3,4МБ
react-window 1.83,8мс8мс1,8МБ

React-window залишається найшвидшим для елементів фіксованої висоти, але йому бракує динамічного вимірювання. TanStack Virtual пропонує найкращий баланс між продуктивністю та функціями. Virtuoso пріоритизує досвід розробника з автоматичною обробкою крайніх випадків.

Оптимізація рендерингу елементів через мемоізацію

Віртуалізація зменшує DOM-вузли, але не може запобігти дорогим повторним рендерам всередині елементів. React.memo стає необхідним:

MemoizedListItem.tsxtypescript
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 дозволяє це:

useScrollRestoration.tstypescript
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

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 7 вересня 2026 р.

Поділитися

Пов'язані статті