Szybka Lista JavaScript w 2026: Windowing, Wirtualizacja i Optymalizacja Wydajności
Poznaj techniki windowing i wirtualizacji do renderowania tysięcy elementów w JavaScript. TanStack Virtual, react-virtuoso i najlepsze praktyki dla wydajnych list w 2026 roku.

Renderowanie tysięcy elementów w liście JavaScript bez wirtualizacji powoduje tworzenie tysięcy węzłów DOM przez przeglądarkę, co prowadzi do wolnego początkowego renderowania, zacinającego się przewijania i zużycia pamięci, które może spowodować awarię urządzeń mobilnych. Szybkie implementacje ListView w JavaScript w 2026 roku rozwiązują ten problem poprzez windowing: renderowanie tylko 10-20 elementów widocznych w viewport, symulując jednocześnie przewijalny obszar zawierający wszystkie elementy.
Windowing renderuje tylko widoczne elementy plus mały bufor (overscan), redukując liczbę węzłów DOM z 10 000 do około 20, niezależnie od rozmiaru listy.
Dlaczego wydajność przeglądarki spada przy dużych listach
Każdy węzeł DOM zużywa pamięć i wymaga od przeglądarki śledzenia jego pozycji podczas obliczeń układu. Lista 10 000 elementów z umiarkowanie złożonym znacznikiem (awatar, dwa spany tekstowe, przycisk) tworzy ponad 40 000 węzłów DOM. Silnik renderujący Chrome przelicza style i pozycje dla wszystkich z nich przy każdym zdarzeniu przewijania.
Rezultat: początkowe renderowanie trwa 2-4 sekundy, przewijanie spada do 15fps, a zużycie pamięci rośnie do ponad 500MB. Na mobilnym Safari karta często całkowicie się zawiesza.
Wirtualizacja naprawia ten problem, utrzymując stałą liczbę węzłów DOM. Niezależnie od tego, czy tablica danych zawiera 100 czy 100 000 elementów, renderowany DOM pozostaje na poziomie około 20-30 węzłów, utrzymując płaskie zużycie pamięci i wydajność przewijania na poziomie 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>
)
}Ta implementacja renderuje tylko widoczne wiersze plus bufor overscan. Zewnętrzny kontener ustawia całkowitą przewijalną wysokość, sprawiając że pasek przewijania zachowuje się tak, jakby wszystkie elementy istniały w DOM.
TanStack Virtual: Bezgłowa architektura na 2026 rok
TanStack Virtual dostarcza framework-agnostyczne prymitywy wirtualizacji. W przeciwieństwie do react-window czy react-virtuoso, nie oferuje gotowych komponentów - tylko hooki obliczające które elementy renderować i gdzie je pozycjonować. Ta bezgłowa architektura oznacza pełną kontrolę nad stylami i strukturą DOM.
Aktualna wersja (3.13) wspiera React, Vue, Solid, Svelte i czysty JavaScript. Kluczowe opcje konfiguracji:
| Opcja | Cel | Domyślna wartość |
|---|---|---|
count | Całkowita liczba elementów | wymagane |
estimateSize | Funkcja zwracająca szacowaną wysokość/szerokość elementu | wymagane |
overscan | Dodatkowe elementy renderowane poza viewport | 1 |
getScrollElement | Zwraca przewijalny kontener | wymagane |
measureElement | Niestandardowa funkcja pomiarowa dla dynamicznych rozmiarów | auto |
Wartość overscan bezpośrednio wpływa na płynność przewijania. Ustawienie zbyt niskiej wartości (0-2) powoduje białe błyski podczas szybkiego przewijania. Ustawienie zbyt wysokiej (20+) niweluje korzyści z wirtualizacji. Dla większości aplikacji, wartość 5-10 zapewnia optymalną równowagę.
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 umożliwia automatyczny pomiar wysokości po renderowaniu. Obsługuje to zmienne długości zawartości bez konieczności ręcznego obliczania wysokości.
react-virtuoso: Bogata w funkcje alternatywa
react-virtuoso przyjmuje przeciwne podejście: bibliotekę komponentów z pełnym wyposażeniem. Tam gdzie TanStack Virtual wymaga budowania kontenera przewijania i logiki pozycjonowania, react-virtuoso obsługuje to wewnętrznie.
Kompromisem jest rozmiar pakietu (37KB vs 12KB dla TanStack Virtual), ale znacznie mniej kodu szablonowego dla typowych wzorców jak listy grupowane, przyklejone nagłówki i interfejsy czatu z odwróconym przewijaniem.
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>
)}
/>
)
}To osiąga ten sam wynik wirtualizacji przy mniejszej ilości kodu. react-virtuoso automatycznie mierzy elementy, obsługuje dynamiczne wysokości i zapewnia zachowanie pozycji przewijania.
Podczas przygotowania do rozmowy kwalifikacyjnej z wydajności React, zrozumienie kiedy wybrać każdą bibliotekę demonstruje świadomość architektoniczną. TanStack Virtual pasuje do aplikacji już używających TanStack Table lub wymagających framework-agnostycznego kodu. react-virtuoso odpowiada zespołom priorytetyzującym szybkość rozwoju nad rozmiarem pakietu.
Gotowy na rozmowy o Node.js / NestJS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Typowe pułapki i rozwiązania
Skoki pozycji przewijania przy obrazach
Obrazy ładują się asynchronicznie, zmieniając wysokość wiersza po początkowym pomiarze. Wirtualizator nie może przewidzieć tej zmiany, powodując przesunięcie zawartości poniżej.
Rozwiązanie: zarezerwowanie miejsca dla obrazów używając aspect-ratio lub stałych wymiarów.
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>
)
}Utrata stanu podczas przewijania
Zwirtualizowane elementy są odmontowywane podczas przewijania poza widoczny obszar. Każdy lokalny stan (pola formularzy, rozwinięte akordeony) znika.
Rozwiązanie: przeniesienie stanu do komponentu nadrzędnego lub biblioteki zarządzania stanem, z kluczem według ID elementu.
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>
)
}Niestabilne klucze powodujące ponowne renderowanie
Używanie indeksów tablicy jako kluczy działa, dopóki lista nie zostanie przesortowana. Elementy otrzymują zły stan, powodując wizualne błędy i zmarnowane renderowania.
Zawsze należy używać stabilnych, unikalnych identyfikatorów z samych danych:
// 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
})}Porównanie bibliotek dla projektów w 2026 roku
Wybór odpowiedniej biblioteki wirtualizacji zależy od wymagań projektu. To porównanie odzwierciedla ekosystem w 2026 roku:
| Kryterium | TanStack Virtual | react-virtuoso | react-window |
|---|---|---|---|
| Rozmiar pakietu | 12KB | 37KB | 6KB |
| Dynamiczne wysokości | Ręczna konfiguracja | Automatycznie | Brak wsparcia |
| TypeScript | Natywne | Natywne | Pakiet @types |
| Wsparcie frameworków | React, Vue, Solid, Svelte | Tylko React | Tylko React |
| Utrzymanie | Aktywne (2026) | Aktywne (2026) | Tryb utrzymania |
| Krzywa uczenia | Wyższa | Niższa | Najniższa |
react-window pozostaje opcją dla prostych list o stałej wysokości, gdzie rozmiar pakietu ma znaczenie. Jednak jego status trybu utrzymania i brak wsparcia dla dynamicznych wysokości czyni go nieodpowiednim dla nowych projektów ze złożonymi wymaganiami.
Dla budowania wydajnych aplikacji React, zrozumienie kompromisów wirtualizacji demonstruje świadomość wydajności, której szukają rekruterzy.
Mierzenie usprawnień wydajności
Kwantyfikacja korzyści z wirtualizacji wymaga spójnej metodologii pomiarowej. Panel Performance w Chrome DevTools dostarcza odpowiednich metryk.
Kluczowe pomiary przed/po wirtualizacji:
- First Contentful Paint (FCP): Czas do pierwszego renderowania zawartości. Wirtualizacja redukuje go poprzez ograniczenie początkowego tworzenia DOM.
- Total Blocking Time (TBT): Blokowanie głównego wątku podczas ładowania. Mniej węzłów DOM oznacza mniej obliczeń stylu/układu.
- Zużycie pamięci: Snapshot rozmiaru sterty w panelu Memory. Powinno pozostać stałe niezależnie od długości listy.
- FPS przewijania: Nagrywanie śladu Performance podczas przewijania. Liczba klatek powinna utrzymywać się na poziomie 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])
}Ten hook loguje FPS przewijania podczas rozwoju. Aplikacje produkcyjne powinny używać narzędzi obserwacyjnych jak Sentry Performance lub niestandardowej telemetrii.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Kluczowe wnioski dla wydajności list JavaScript
- Wirtualizacja redukuje węzły DOM z tysięcy do około 20, utrzymując stałą pamięć i wydajność przewijania niezależnie od rozmiaru danych
- TanStack Virtual (12KB) dostarcza bezgłowe prymitywy dla maksymalnej kontroli w React, Vue, Solid i Svelte
- react-virtuoso (37KB) oferuje gotowe komponenty z automatyczną obsługą dynamicznych wysokości
- react-window pozostaje w trybie utrzymania od 2022 roku i nie wspiera dynamicznych wysokości
- Ustawienie overscan między 5-10 zapewnia optymalną płynność przewijania bez niwelowania korzyści z wirtualizacji
- Rezerwowanie wymiarów obrazów zapobiega skokom pozycji przewijania podczas asynchronicznego ładowania
- Przeniesienie stanów rozwijanych/edytowalnych do komponentu nadrzędnego, ponieważ zwirtualizowane wiersze są odmontowywane podczas przewijania
- Używanie stabilnych kluczy z ID danych, nigdy indeksów tablicy, aby zapobiec niezgodnościom stanu przy przesortowaniu
- Mierzenie FCP, TBT, pamięci i FPS przewijania przed/po implementacji w celu kwantyfikacji usprawnień
Znajdziesz błąd w Node.js / NestJS?
Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Autor:
Anthony Fillion-MailletZałożyciel SharpSkill
Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.
Zaktualizowano 15 września 2026
Udostępnij
Powiązane artykuły

NestJS i Redis w 2026: Cachowanie, Sesje i Pytania Rekrutacyjne
Kompleksowy przewodnik po integracji Redis z NestJS obejmujący cache-manager, zarządzanie sesjami i strategie invalidacji.

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.

NestJS i MongoDB w 2026: Mongoose, Agregacje i Pytania Rekrutacyjne
Opanuj NestJS z MongoDB i Mongoose 9. Poznaj wzorce projektowania schematów, potoki agregacji i przygotuj się do rozmów kwalifikacyjnych z praktycznymi przykładami.