Швидкий JavaScript ListView у 2026: Windowing, Віртуалізація та Оптимізація Продуктивності

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

Швидкий JavaScript ListView у 2026: Windowing, Віртуалізація та Оптимізація Продуктивності

Рендеринг тисяч елементів у JavaScript ListView без віртуалізації змушує браузер створювати тисячі DOM-вузлів, що призводить до повільного початкового рендерингу, лагів при прокручуванні та споживання пам'яті, яке може спричинити збій мобільних пристроїв. Швидкі реалізації JavaScript ListView у 2026 році вирішують цю проблему через windowing: рендеринг лише 10-20 елементів, видимих у viewport, одночасно симулюючи область прокрутки, що містить усі елементи.

Windowing одним реченням

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.

VirtualizedList.tsxtypescript
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Додаткові елементи, що рендеряться за межами viewport1
getScrollElementПовертає контейнер прокруткиобов'язковий
measureElementВласна функція вимірювання для динамічних розмірівавто

Значення overscan безпосередньо впливає на плавність прокрутки. Занадто низьке значення (0-2) спричиняє білі спалахи під час швидкої прокрутки. Занадто високе (20+) нівелює переваги віртуалізації. Для більшості застосунків 5-10 забезпечує оптимальний баланс.

DynamicHeightList.tsxtypescript
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), але значно менше шаблонного коду для типових патернів, як-от групові списки, липкі заголовки та інтерфейси чату зі зворотною прокруткою.

VirtuosoList.tsxtypescript
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 або фіксованих розмірів.

ImageListItem.tsxtypescript
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 елемента.

ExpandableList.tsxtypescript
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>
  )
}

Нестабільні ключі, що спричиняють повторні рендери

Використання індексів масиву як ключів працює, поки список не буде пересортовано. Елементи отримують неправильний стан, спричиняючи візуальні помилки та марні рендери.

Завжди слід використовувати стабільні, унікальні ідентифікатори з самих даних:

typescript
// 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 Virtualreact-virtuosoreact-window
Розмір пакету12KB37KB6KB
Динамічні висотиРучне налаштуванняАвтоматичноНе підтримується
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.
PerformanceMonitor.tsxtypescript
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

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

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

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

Поділитися

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