Вузьке місце DOM, про яке ніхто не говорить

Уявіть собі панель підтримки, яка підтягує десять тисяч записів логів. Або CRM, що намагається відобразити кожен контакт в одній таблиці з прокруткою. У React код для створення такого виглядає цілком безневинно. Ви проходите по масиву через map, повертаєте JSX і дозволяєте фреймворку робити свою роботу. У режимі розробки зі ста рядками все працює чудово. Але коли приходять реальні дані, сторінка стає некеровано повільною.

Браузер не лінується. Він робить саме те, про що ви його попросили, і в цьому полягає проблема. Кожен рядок стає DOM-вузлом. Кожен вузол отримує стилі, розраховує макет (layout), малюється (paint) та відстежується в пам'яті. Коли ви прокручуєте сторінку, браузер перераховує позиції для всього дерева, а не лише для тієї частини, яку ви бачите. Обробники подій накопичуються. Використання пам'яті різко зростає. Зрештою, головний потік (main thread) перевантажується настільки, що інтерфейс перестає реагувати на кліки, натискання клавіш або навіть на саму прокрутку. Технічно застосунок не «впав», але для користувача, який сидить перед екраном, робота стає неможливою.

Це стається тому, що браузер намагається тримати кожен окремий елемент в активній пам'яті одночасно. React може бути ефективним у створенні віртуальних описів вашого інтерфейсу, але як тільки ці описи стають реальними вузлами в документі, вони коштують стільки ж, скільки написаний вручну HTML. У самого фреймворка немає «швидкого рішення» (escape hatch). Вам потрібна структурна зміна в тому, як ви передаєте список у DOM.

Що насправді означає віртуалізація

Віртуалізація — це і є та структурна зміна. Замість того, щоб просити React відрендерити весь масив, ви рендерите лише ті елементи, які можуть поміститися у в'юпорт (viewport), плюс невеликий буфер зверху та знизу. Коли користувач прокручує сторінку, застосунок видаляє вузли, що виходять із поля зору, і створює нові, які з'являються з протилежного краю. Для користувача це все одно виглядає як один безперервний список, оскільки загальна висота прокрутки зберігається — зазвичай за допомогою одного високого контейнера або ретельно розрахованого елемента-розпірки (spacer). Видимі елементи — це просто вікно, що ковзає по набору даних.

Уявіть це як кіноплівку, що проходить крізь проєкторний механізм. Глядачі бачать плавний рух, але механізм підсвічує лише той кадр, який зараз знаходиться в позиції. Решта котушки знаходиться на подачі та приймальному валу, а не на шляху світла. Віртуалізовані списки працюють так само. Набір даних — це котушка. В'юпорт — це проєкційний механізм.

Це не «ліниве завантаження» (lazy loading) у традиційному розумінні. Lazy loading відкладає отримання даних до того моменту, поки користувач не наблизиться до них. Віртуалізація ж передбачає, що дані у вас уже є, але ви вибірково вирішуєте, які саме фрагменти перетворювати на реальні DOM-елементи. Ці дві техніки можуть працювати разом, але вони вирішують різні проблеми.

Чому різниця відчувається миттєво

Переваги проявляються у чотирьох аспектах, і всі вони пов'язані з одним і тим самим відчуттям полегшення: ви перестаєте витрачати ресурси на те, чого користувач не бачить.

Швидший початковий час завантаження. Коли браузер відкриває сторінку, він малює, можливо, п'ятнадцять рядків замість п'ятнадцяти тисяч. Перший значущий малюнок (first meaningful paint) з'являється швидше. Час до інтерактивності (time-to-interactive) зменшується, оскільки JavaScript-двигун витрачає менше часу на створення вузлів та їх прикріплення до документа.

Менше використання пам'яті. DOM-вузол — це «дорогий» об'єкт. Кожен із них містить посилання на правила стилів, метрики макета та прив'язки подій. Якщо скоротити кількість активних вузлів до кількох десятків, обсяг споживаної пам'яті різко зменшиться. На слабких

First, the container needs a defined height. If the list sits inside a parent that expands to fit its children, virtualization cannot calculate which items are visible because there is no viewport boundary. You must lock the list into a fixed height or a flex container with known constraints.

Second, item sizing matters immensely. Fixed-height rows are the simplest case. The library multiplies the row height by the index and knows exactly where to position each element. Variable-height content, like chat messages with embedded images or comment threads, forces the library to measure after mount and adjust on the fly. That measurement step can cause scroll jitter if it happens too late. If your data allows it, enforce uniform heights or minimum heights. If not, use a variable-height virtualizer and accept the extra complexity.

Third, overscanning is your friend. Rendering exactly what fits on screen produces blank white strips when the user scrolls quickly. Most libraries let you render a few extra items above and below the fold. Two or three rows of overscan is usually enough to hide the seams without bloating the DOM again.

Fourth, do not ignore the key prop. In a virtualized list, items reuse DOM nodes as you scroll. Stable keys prevent React from guessing incorrectly during reconciliation and destroying state inside row components. If your list rows contain inputs, toggles, or expandable sections, bad keys will corrupt the UI state in ways that look like bugs in your data layer but are actually rendering mistakes.

One subtle trap is browser find-in-page. Because the hidden items do not exist in the DOM, the browser's search box will not see them. If your users rely on Ctrl+F to locate text inside a large list, you will need to build a custom search that operates against the dataset, not the document. Screen readers can also lose context if the list semantics are not handled carefully, so test with assistive technology and consider adding live region announcements for dynamic loading.

When You Should Skip It

Virtualization is not free. It adds dependency weight, coordinate math, and constraint overhead. If your list tops out at fifty or a hundred items, the browser can handle that without help. Render the whole thing and move on. The same applies if your list items are extremely complex individually. Virtualization saves you from thousands of nodes, but it cannot save you from one node that contains a massive chart or video element. Fix the item bloat first.

Also avoid virtualization when the list does not scroll. If you are paginating with next and previous buttons and only showing twenty items per page, there is nothing to window. The technique only earns its keep when the user expects to scroll through a large contiguous sequence.

The Real Takeaway

Virtualization is less a library choice and more a mindset. It forces you to acknowledge that the DOM is a finite resource, not an infinite canvas. Before you add it, open Chrome DevTools, record a performance profile, and confirm that layout or paint time is actually the culprit. Once you know the DOM is the bottleneck, commit to the constraints. Lock your heights, watch your keys, overscan modestly, and test your accessibility. Done right, a virtualized list turns an unusable data wall into something that feels as light as a native scroll view. The browser stops fighting, your users stop waiting, and the app finally behaves like the fast interface you intended to build.