Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке віртуалізація і навіщо вона потрібна». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Віртуалізація** рендерить лише видимі у viewport DOM-вузли, переставляючи пул із ~20-50 елементів під час прокрутки, замість того щоб створювати вузол для кожного запису списку. ```jsx import { FixedSizeList as List } from 'react-window'; <List height={400} itemCount={10000} itemSize={35}> {({ index, style }) => <div style={style}>Item {index}</div>} </List> // ~20 DOM-вузлів при будь-якій позиції прокрутки, не 10 000 ``` **Ключове:** вмикай коли список більше 500 елементів і прокрутка починає гальмувати.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Віртуалізація** рендерить лише ті DOM-вузли, які видно у viewport, і переставляє фіксований пул із ~20-50 елементів під час прокрутки, незалежно від реальної кількості записів у списку. ## Теорія ### TL;DR - Аналогія: театр із 20 місцями, де актори виходять через бічні двері зі зміщенням прожектора. 10 000 місць будувати не потрібно. - Повний рендер: 10 000 DOM-вузлів для 10 000 елементів. Зі віртуалізацією: 20-50 вузлів, які переставляються. - Пам'ять падає з 100+ МБ до ~2 МБ для того самого набору даних. - Початкове завантаження скорочується з 2+ с до ~100 мс. - Вмикай коли прокручуваний список більше 500 елементів і scroll гальмує; пропускай для коротких статичних списків. ### Швидкий приклад ```jsx // Без віртуалізації: 10 000 DOM-вузлів, браузер гальмує при прокрутці const SlowList = ({ items }) => ( <ul style={{ height: '400px', overflow: 'auto' }}> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> ); // З react-window: ~20 вузлів незалежно від items.length import { FixedSizeList as List } from 'react-window'; const FastList = ({ items }) => ( <List height={400} itemCount={items.length} itemSize={35}> {({ index, style }) => <div style={style}>{items[index].name}</div>} </List> ); // Відкрий DevTools при прокрутці: ті самі ~20 div переставляються, нові не додаються ``` Бібліотека позиціонує кожен видимий рядок через `transform: translateY()`, тому браузер пропускає reflow і тримає 60fps протягом усієї прокрутки. ### Як працює пул DOM-елементів Браузер визначає, які рядки потрапляють у viewport через `getBoundingClientRect()` або `ResizeObserver`. Бібліотека тримає невеликий буфер (overscan) - зазвичай 3-5 рядків вище і нижче видимої зони. Коли користувач прокручує, наявні DOM-вузли отримують новий вміст і новий `translateY`-офсет. Жодних нових вузлів не створюється і не видаляється під час прокрутки. React звіряє вузли за `key` кожного virtual item. Якщо пропустити key, React перерендерить усі рядки при кожному скролі. Ця одна помилка опускає frame rate із 60fps до 10fps і трапляється на code review частіше, ніж хотілося б. ### Коли використовувати - Список або таблиця більше 500 елементів -> віртуалізуй. - Безкінечне прокручування (inbox, стрічка новин) -> завантажуй частинами і віртуалізуй видиму частину. - Деревоподібні структури на кшталт файлового менеджера -> віртуалізуй тільки розгорнуті гілки. - Мобільний застосунок зі списком більше 200 елементів -> завжди (пам'яті на телефоні менше). - Статичний список менше 200 елементів -> пропускай. Бібліотека додає ~10 кБ без реальної вигоди. ### Порівняльна таблиця | | Повний рендер | Віртуалізація | |---|---|---| | DOM-вузли (10 000 елементів) | 10 000 | 20-50, переставляються | | Прокрутка 60fps | Ні | Так | | Пам'ять (10 000 елементів) | 100+ МБ | ~2 МБ | | Початкове завантаження | 2+ с | ~100 мс | | Підходить для | <500 статичних елементів | >500 динамічних або прокручуваних списків | ### Типові помилки **Немає `key` на virtual items.** React потребує стабільний ключ, щоб розуміти, який елемент відповідає якому DOM-вузлу. У розробці список виглядає нормально, але в продакшені падає до 10fps, коли дані часто оновлюються. ```jsx // Неправильно - повний перерендер при кожній прокрутці {virtualItems.map(item => <div>{data[item.index]}</div>)} // Правильно {virtualItems.map(item => <div key={item.key}>{data[item.index]}</div>)} ``` **Нульовий overscan при швидкій прокрутці.** Коли користувач різко свайпає, між рядками з'являються білі смуги. ```jsx // Неправильно - білі прогалини при швидкому флінгу const virtualizer = useVirtualizer({ count: items.length, overscan: 0 }); // Правильно - 5 зайвих рядків вище і нижче viewport const virtualizer = useVirtualizer({ count: items.length, overscan: 5 }); ``` **Фіксована висота рядка для вмісту зі змінною висотою.** Повідомлення з картинками ламають розмітку, якщо вважати кожен рядок за 50px. ```jsx // Неправильно - прогалини та зміщення при емодзі або зображеннях const virtualizer = useVirtualizer({ estimateSize: () => 50 }); // Правильно - вимірюй реальні висоти під час рендеру const virtualizer = useVirtualizer({ count: messages.length, estimateSize: () => 50, measureElement: el => el?.getBoundingClientRect()?.height || 50, }); ``` **Віртуалізація малих списків.** Підключати `react-window` для 150 елементів - це 10 кБ JS без реальної вигоди. Просто `.map()`. **Вкладена віртуалізація.** Virtual list всередині virtual table дає подвійні `translateY`-трансформи, які переповнюються і ламають позиціонування. Вирівняй структуру або використай `position: sticky` для заголовків. ### Де використовується - Jira і Gmail web використовують для списків задач та inbox із 10 000+ рядків. - TanStack Table v8+ підтримує virtual rows через `@tanstack/react-virtual`. - Ant Design Table всередині використовує `vc-virtual-list` для enterprise-дашбордів. - Vercel admin panels віртуалізують вивід логів при безкінечному прокручуванні. - Для нових проектів краще `@tanstack/react-virtual` (активно підтримується, ~5 кБ) ніж `react-virtualized` (legacy, важча). ### Питання на співбесіді **Q:** У чому різниця між virtualization і windowing? **A:** Це одне й те саме. "Windowing" - старий термін із часів `react-virtualized`, зараз у спільноті кажуть "virtualization". **Q:** Як `react-window` підтримує змінні висоти рядків? **A:** Через `VariableSizeList` з функцією `getItemSize(index)`. Бібліотека кешує виміряні висоти і перераховує scroll-офсети при зміні розміру контейнера. **Q:** Як React 18 впливає на продуктивність віртуалізації? **A:** `startTransition` огортає оновлення обробника прокрутки, роблячи їх низькопріоритетними. Заїкання зникає навіть коли рядки містять важкі компоненти, бо React може перервати оновлення заради пріоритетніших подій. **Q:** Чому не просто CSS `contain: layout` замість видалення DOM-вузлів? **A:** `contain` підказує браузеру пропустити перерахунок стилів за межами контейнера, але вузли залишаються в DOM. Для 50 000 рядків це не рятує від проблем із пам'яттю. **Q (senior-рівень):** `ResizeObserver` ламає віртуалізацію в Safari 15-16. Як виправити без заміни бібліотеки? **A:** Додай поліфіл через `MutationObserver` на scroll-контейнер, throttle `scroll`-обробника до 16 мс і додай `window.resize` як fallback. Цей баг - відоме питання на мобільних співбесідах у FAANG. ## Приклади ### Базовий: фіксована висота рядка з react-window ```jsx import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>Користувач #{index}</div> ); function UserList({ users }) { return ( <List height={400} itemCount={users.length} itemSize={35} width="100%"> {Row} </List> ); } // У DOM ~20 рядків при будь-якій позиції прокрутки ``` Кожен рядок 35px заввишки. `itemSize` має збігатися з реальною CSS-висотою, інакше позиція прокрутки зміщується. Відкрий DevTools при скролі - побачиш одні й ті самі ~20 div, які переставляються, а не нові. ### Продакшен: динамічний список через TanStack Virtual ```jsx import { useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; function IssueList({ issues }) { // наприклад, 50 000 задач із API const parentRef = useRef(); const virtualizer = useVirtualizer({ count: issues.length, getScrollElement: () => parentRef.current, estimateSize: () => 40, overscan: 5, }); return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}> {virtualizer.getVirtualItems().map(item => ( <div key={item.key} style={{ position: 'absolute', top: 0, transform: `translateY(${item.start}px)`, width: '100%', }} > {issues[item.index].title} </div> ))} </div> </div> ); } // ~30 рядків у DOM при прокрутці 50 000 задач ``` Зовнішній `div` отримує повну віртуальну висоту, тому scrollbar точно відображає прогрес у списку. Кожен видимий рядок позиціонується через `translateY` - без перерахунку layout. Це той самий патерн, який використовують TanStack Table v8 і Shadcn-дашборди.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.