Рендеринг больших списков в вебе: системное сравнение CSS content-visibility и виртуализации DOM
Отображение многотысячных наборов данных — каталогов товаров, лент сообщений, таблиц аналитики и системных логов — неизбежно сталкивает фронтенд-разработчиков с проблемой снижения отзывчивости интерфейса. Когда страница пытается одновременно отобразить тысячи сложных карточек, браузер начинает пропускать кадры при прокрутке, зависает при первичной загрузке и потребляет сотни мегабайт оперативной памяти.
Для решения этой проблемы в современной веб-разработке применяются два принципиально разных подхода: декларативная оптимизация с помощью нативного CSS-свойства content-visibility и скриптовая виртуализация дерева DOM (механизм Windowing). Выбор между ними — это не просто сравнение производительности, а поиск баланса между сложностью архитектуры приложения и сохранением нативных возможностей браузера.
Причины деградации производительности: раскладка против памяти
Чтобы понять разницу между подходами, необходимо разделить два независимых источника нагрузки на браузерный движок:
- Дерево DOM (Document Object Model) — объектное представление структуры документа в оперативной памяти. Наряду с DOM браузер параллельно строит дерево доступности (Accessibility Tree) для скринридеров и вспомогательных технологий.
- Процессы Layout и Paint — этапы вычисления геометрических размеров каждого элемента на экране (Layout) и последующей отрисовки пикселей в слои графического процессора (Paint).
Когда приложение рендерит обычный список из 10 000 карточек без оптимизаций, основное время тратится на расчет геометрии и перерисовку блоков, которые пользователь в данный момент даже не видит. Однако если каждая карточка содержит десяток вложенных элементов (иконки, кнопки, текстовые поля), то 50 000 карточек создают свыше 550 000 узлов в памяти. В такой ситуации главным узким местом становится уже сам объем дерева элементов и расход оперативной памяти.
Путь CSS: как работает content-visibility и плейсхолдеры размеров
Свойство content-visibility: auto, описанное в спецификации CSS Containment, позволяет браузеру пропускать расчет раскладки и отрисовку внеэкранного содержимого, пока пользователь не приблизится к нему при прокрутке.
Ключевая особенность этого подхода: элементы продолжают физически существовать в дереве DOM, но браузер временно изолирует их рендеринг. Чтобы страница не схлопывалась в нулевую высоту при пропуске невидимых блоков, используется парное свойство contain-intrinsic-size:
.list-item {
content-visibility: auto;
contain-intrinsic-size: auto 120px;
}
Конструкция auto <length> задает начальную оценочную высоту блока (например, 120 пикселей). Как только пользователь доскролливает до карточки и браузер выполняет ее реальную отрисовку, движок запоминает точный фактический размер элемента и использует его при последующих вычислениях. Это предотвращает скачки полосы прокрутки (Layout Shifts).
Официальная документация MDN content-visibility и MDN contain-intrinsic-size подчеркивает главные преимущества CSS-маршрута:
- Нативный поиск по странице (Ctrl/Cmd+F): поскольку текст присутствует в DOM, встроенный поиск браузера находит совпадения и автоматически прокручивает страницу к нужному блоку.
- Прямые якорные ссылки: переход по URL с фрагментом
#item-500нативно фокусирует и отображает целевой элемент. - Доступность: структура документа остается целостной для скринридеров и навигации клавишей Tab.
- Сохранение состояния: открытые интерактивные спойлеры
<details>, выделение текста и фокус ввода не сбрасываются при скролле.
При этом разработчикам необходимо учитывать особенности поддержки в различных браузерных движках (например, исторические задержки синхронизации поиска в Safari) и обязательно тестировать стабильность скролла при карточках с сильно различающейся высотой.
Путь виртуализации: экономия памяти и исчезновение узлов
Виртуализация списков (реализуемая через библиотеки вроде @lit-labs/virtualizer, TanStack Virtual или react-window) действует иначе: она физически удаляет из дерева DOM все узлы, находящиеся за пределами экрана.
Виртуализатор отслеживает позицию прокрутки и монтирует в DOM только элементы, попадающие в видимую область просмотра (viewport), плюс небольшой буфер вокруг нее (overscan, обычно около 800–1000 пикселей). При скролле карточки, выходящие за пределы буфера, уничтожаются (unmount), а новые узлы создаются или переиспользуются.
В результате список из 100 000 записей потребляет в памяти ресурсы всего для нескольких десятков реальных DOM-элементов. Время первой отрисовки держится на уровне долей миллисекунды независимо от общего размера массива данных.
Однако физическое отсутствие узлов в документе разрушает базовые контракты браузера:
- Текст вне экрана не находится стандартным поиском браузера.
- Запрос
document.getElementById('item-500')возвращаетnull. - Фокус при перемещении по клавише Tab обрывается на границе видимой зоны.
- Любое неконтролируемое состояние внутри компонента (введенный текст в инпуте, сдвиг курсора, прогресс воспроизведения медиафайла) безвозвратно теряется при демонтаже узла.
Ответственность приложения и доступность интерфейса
Распространенный миф гласит, что виртуализация якобы «ломает доступность». В действительности смонтированные в окне просмотра строки остаются семантически корректными. Однако виртуализация перекладывает всю ответственность за навигационные сценарии с браузера на код приложения:
- Поиск по данным: приложение обязано реализовать собственный поисковый интерфейс по исходной модели данных в памяти, рассчитывать индекс найденного элемента и отдавать команду виртуализатору на перемещение к нему.
- Асинхронные глубокие ссылки: переход к элементу требует программного вызова API виртуализатора, ожидания завершения пересчета раскладки через
ResizeObserver, монтирования узла в DOM и явного вызова метода.focus(). При этом обязательно настраивается защитный таймаут на случай срыва анимации. - Вынесение состояния в глобальную модель: все флаги активности, выбор чекбоксов и раскрытие аккордеонов должны храниться во внешнем хранилище состояния, а не в локальном DOM-узле.
- Стабильные ключи рендеринга: привязка карточек к числовым индексам массива недопустима, так как при сортировке или фильтрации это приводит к смешиванию состояний между разными сущностями.
Анализ бенчмарков и границы применимости
Инженерные замеры на синтетических наборах данных (100, 1 000 и 10 000 элементов) наглядно иллюстрируют различия:
- На 1 000 карточек
content-visibility: autoускоряет первичную отрисовку в несколько раз по сравнению с обычным списком, снижая нагрузку на процессор без усложнения кодовой базы. - На 10 000 и более элементов время создания узлов для CSS-маршрута начинает расти линейно, так как память тратится на сотни тысяч элементов. Виртуализация же сохраняет стабильную скорость рендеринга менее 1 миллисекунды.
При этом жесткого порога по количеству строк не существует: 500 сложных интерактивных карточек с графиками и анимациями могут создавать большую нагрузку, чем 10 000 простых текстовых строк.
Практическая матрица выбора
При выборе архитектурного решения рекомендуется опираться на дерево решений:
- Выбирайте CSS
content-visibility: auto, если вы разрабатываете каталог товаров, ленту статей или страницу с умеренным объемом данных (до нескольких тысяч элементов), где критически важен нативный поиск браузера, прямые якорные ссылки, простота разметки и надежная работа вспомогательных технологий без написания сложного связующего кода. - Выбирайте виртуализацию DOM, если профилирование в DevTools фиксирует критический дефицит памяти из-за гигантских коллекций (десятки тысяч строк, тяжелые таблицы логов, бесконечные data-grid), а команда готова спроектировать и поддерживать поиск по данным, программное управление фокусом и внешнее хранение состояний компонентов.
Грамотная оптимизация начинается с замеров на самых медленных целевых устройствах пользователей: это позволяет выбрать минимально необходимый уровень сложности архитектуры для достижения стабильных 60 кадров в секунду.

