Wydajna lista JavaScript ListView w 2026: Techniki wirtualizacji i optymalizacji

Opanuj wirtualizację list JavaScript z TanStack Virtual, react-virtuoso i react-window. Renderuj ponad 100 tysięcy elementów płynnie dzięki technikom windowing.

Wydajna lista JavaScript ListView w 2026: Techniki wirtualizacji i optymalizacji

Wydajne renderowanie list JavaScript ListView staje się krytyczne, gdy aplikacje muszą wyświetlać tysiące elementów bez zamrażania przeglądarki. Wirtualizacja list, technika renderowania tylko widocznych elementów przy jednoczesnym recyklingu węzłów DOM, przekształca wolne przewijanie w płynne interakcje.

Podstawowa zasada wirtualizacji

Wirtualizacja renderuje tylko elementy widoczne w viewport plus mały bufor overscan. Lista 100 000 elementów z 10 widocznymi wierszami tworzy zaledwie 15-20 węzłów DOM zamiast 100 000, redukując zużycie pamięci z setek megabajtów do kilobajtów.

Dlaczego liczba węzłów DOM niszczy wydajność przewijania

Przeglądarki mają problemy z dużymi drzewami DOM. Każdy węzeł wymaga pamięci dla samego elementu, jego obliczonych stylów i danych layoutu. Powyżej 5000 węzłów reflow stają się zauważalne. Przy 50 000 węzłach zdarzenia przewijania mogą zajmować ponad 100ms, powodując widoczne opóźnienia.

Koszt wydajnościowy rośnie nieliniowo. Pipeline renderowania Chrome musi:

  1. Wykonać handlery zdarzeń JavaScript
  2. Przeliczyć style dla dotkniętych elementów
  3. Obliczyć pozycje layoutu
  4. Narysować piksele na warstwach
  5. Skomponować warstwy do finalnego wyświetlenia

Wirtualizacja całkowicie eliminuje kroki 2-4 dla elementów poza ekranem. Przeglądarka utrzymuje stały rozmiar DOM niezależnie od długości danych.

TanStack Virtual: Standard wirtualizacji bez interfejsu

TanStack Virtual dostarcza prymitywy wirtualizacji niezależne od frameworka. Wersja 3.13 (wydana w sierpniu 2026) przyniosła znaczące usprawnienia wydajności: zimny mount dla 100 000 elementów spadł z 6,1ms do 4,5ms, a najgorszy scenariusz resize poprawił się z 2 sekund do 1,3 milisekundy.

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, // Renderuj 5 dodatkowych elementów powyżej/poniżej 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 używa transform: translateY() zamiast pozycjonowania top. Uruchamia to kompozycję GPU zamiast przeliczania layoutu, utrzymując przewijanie na poziomie 60fps nawet przy złożonym renderowaniu elementów.

Dynamiczne wysokości elementów z automatycznym pomiarem

Elementy o stałej wysokości upraszczają matematykę wirtualizacji, ale rzeczywiste aplikacje często wymagają zmiennych wysokości. TanStack Virtual mierzy elementy automatycznie używając 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, []), // Początkowe oszacowanie
    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 otrzymuje każdy wyrenderowany element i zwraca jego zmierzoną wysokość. TanStack Virtual cache'uje te pomiary i ponownie mierzy tylko gdy ResizeObserver wykryje zmiany.

React Virtuoso: Gotowy do produkcji od razu

React Virtuoso (wersja 4.18, wrzesień 2026) oferuje API wyższego poziomu, które automatycznie obsługuje przypadki brzegowe. Z 2,8 miliona pobrań tygodniowo na npm, stał się domyślnym wyborem dla zespołów chcących minimalnej konfiguracji.

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 obsługuje zmienne wysokości, zgrupowane elementy i odwrotne przewijanie bez dodatkowej konfiguracji. Dla aplikacji czatowych lub przeglądarek logów wymagających dodawania elementów na początku przy zachowaniu pozycji przewijania, działa to płynnie:

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} // Offset wirtualnego indeksu
      initialTopMostItemIndex={messages.length - 1}
      followOutput="smooth" // Auto-scroll do nowych wiadomości
      startReached={() => loadOlderMessages().then(prependMessages)}
      itemContent={(index, message) => <ChatBubble message={message} />}
    />
  )
}

Właściwość firstItemIndex umożliwia dwukierunkowe nieskończone przewijanie. Virtuoso utrzymuje pozycję przewijania podczas dodawania elementów na początku poprzez dostosowywanie wirtualnych indeksów zamiast przesuwania pozycji DOM.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Porównanie wydajności: TanStack vs Virtuoso vs react-window

Benchmarki na średniej klasy laptopie z 2026 roku (M3 MacBook Air) ze 100 000 elementów:

BibliotekaPoczątkowy mountPrzewiń do środkaZużycie pamięci
TanStack Virtual 3.134,5ms12ms2,1MB
react-virtuoso 4.186,2ms15ms3,4MB
react-window 1.83,8ms8ms1,8MB

React-window pozostaje najszybszy dla elementów o stałej wysokości, ale brakuje mu dynamicznego pomiaru. TanStack Virtual oferuje najlepszą równowagę między wydajnością a funkcjami. Virtuoso priorytetyzuje doświadczenie programisty z automatyczną obsługą przypadków brzegowych.

Optymalizacja renderowania elementów przez memoizację

Wirtualizacja redukuje węzły DOM, ale nie może zapobiec kosztownym ponownym renderom wewnątrz elementów. React.memo staje się niezbędny:

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) => {
  // Własne porównanie: renderuj ponownie tylko gdy dane elementu się zmieniają
  return prevProps.item.id === nextProps.item.id &&
         prevProps.item.name === nextProps.item.name
})

Funkcja własnego porównania zapobiega ponownym renderom gdy zmienia się tylko referencja, ale zawartość pozostaje identyczna, co jest częste przy wzorcach pobierania danych zwracających nowe referencje obiektów.

Obsługa przywracania pozycji przewijania

Użytkownicy oczekują zachowania pozycji przewijania przy nawigacji tam i z powrotem. History API w połączeniu ze stanem virtualizera umożliwia to:

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)

  // Przywróć przy montowaniu
  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])

  // Zapisz przed odmontowaniem
  useEffect(() => {
    return () => {
      const offset = virtualizer.scrollOffset
      sessionStorage.setItem(storageKey, JSON.stringify({ offset }))
    }
  }, [virtualizer, storageKey])
}

Ten wzorzec przechowuje offset przewijania w sessionStorage, przetrwający nawigację między stronami, ale czyszczony przy zamknięciu karty.

Tematy rozmów kwalifikacyjnych: Pytania rekruterów o wirtualizację list

Rozmowy techniczne coraz częściej obejmują wiedzę o wirtualizacji. Typowe pytania i dobre odpowiedzi:

P: Dlaczego przeglądarki nie radzą sobie efektywnie ze 100k węzłów DOM? Pipeline renderowania przetwarza każdy węzeł podczas obliczania stylów i layoutu. Presja pamięciowa od obiektów węzłów, obliczonych stylów i drzew layoutu kumuluje się. Powyżej 10k węzłów pauzy garbage collection stają się zauważalne podczas przewijania.

P: Wyjaśnij windowing vs recycling. Windowing renderuje tylko widoczne elementy. Recycling ponownie używa istniejących węzłów DOM dla nowych danych gdy elementy przewijają się poza widok. TanStack Virtual i react-virtuoso używają windowing. Natywne frameworki mobilne (iOS UITableView, Android RecyclerView) używają recyclingu.

P: Jak zwirtualizowałbyś siatkę obrazów? Użyj useVirtualizer z TanStack Virtual z wymiarami zarówno wierszy jak i kolumn. Zaimplementuj estimateSize dla obu osi. Dodaj lazy loading obrazów z IntersectionObserver. Rozważ CSS content-visibility: auto dla elementów blisko krawędzi viewport.

Więcej przygotowań do rozmów kwalifikacyjnych na tematy wydajności frontendowej można znaleźć w module pytań do rozmowy o testowaniu React, który obejmuje powiązane wzorce optymalizacji.

Typowe pułapki i jak ich unikać

Nieprawidłowa wysokość kontenera: Wirtualizacja wymaga kontenera przewijania o stałej wysokości. Używanie height: auto lub poleganie na wysokości zawartości psuje obliczenia pomiarów.

Brakujące klucze: React potrzebuje stabilnych kluczy dla elementów podlegających recyklingowi. Używanie indeksów tablicy powoduje błędy stanu gdy elementy zmieniają kolejność.

Zbyt niski overscan: Zerowy overscan powoduje widoczne puste obszary podczas szybkiego przewijania. Wartości między 3-10 równoważą wydajność i wizualną płynność.

Niezapamiętane callbacki: Przekazywanie funkcji inline jako propsów elementów wywołuje ponowne rendery przy każdym przewijaniu. Wyodrębnij handlery do useCallback lub zakresu komponentu.

Kluczowe wnioski dotyczące wirtualizacji list JavaScript

  • Liczba węzłów DOM, nie rozmiar danych, określa wydajność przewijania. Wirtualizacja ogranicza węzły niezależnie od długości listy.
  • TanStack Virtual 3.13 pasuje do aplikacji wymagających elastyczności frameworka i maksymalnej kontroli. Wydajność zimnego montowania poprawiła się o 35% w porównaniu z poprzednimi wersjami.
  • react-virtuoso 4.18 obsługuje dynamiczne wysokości, grupowanie i dwukierunkowe przewijanie przy minimalnym kodzie. 2,8M pobrań tygodniowo odzwierciedla stabilność produkcyjną.
  • react-window pozostaje opłacalny dla scenariuszy ze stałą wysokością gdzie rozmiar bundle ma znaczenie, choć aktywny rozwój ustał.
  • Memoizuj elementy listy z funkcjami własnego porównania. Sprawdzanie równości referencji zawodzi gdy źródła danych tworzą nowe obiekty.
  • Przechowuj offset przewijania w sessionStorage dla przywracania między nawigacjami. Offset virtualizera mapuje się bezpośrednio na pozycję piksela.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w Node.js / NestJS?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 7 września 2026

Udostępnij

Powiązane artykuły