Back

Виртуальная прокрутка для высокопроизводительных интерфейсов

Виртуальная прокрутка для высокопроизводительных интерфейсов

Попытайтесь отрендерить 500 000 строк в браузере — и ваш интерфейс, скорее всего, зависнет, начнёт тормозить или вовсе упадёт. Хотя современные браузеры могут работать с большими DOM-деревьями, производительность часто резко падает по мере роста количества узлов из-за затрат на компоновку, вычисление стилей и потребление памяти. Виртуальная прокрутка решает эту проблему, рендеря только то, что пользователь действительно видит — и это единственное ограничение полностью меняет то, как работают интерфейсы с большими объёмами данных.

Ключевые выводы

  • Виртуальная прокрутка рендерит только элементы, видимые во viewport (плюс небольшой буфер), сохраняя количество DOM-узлов постоянным независимо от размера набора данных.
  • Механизм работает путём вычисления видимых индексов на основе scrollTop, рендеринга этих элементов и использования элементов-заполнителей для имитации полной прокручиваемой высоты.
  • Фиксированная высота элементов упрощает реализацию. Динамическая высота требует кэширования измерений и тщательной коррекции позиции прокрутки.
  • Встроенный браузерный поиск (Ctrl+F), доступность и стабильность позиции прокрутки требуют дополнительного внимания в виртуализированных списках.
  • Для React, Angular и Vue существуют зрелые библиотеки — создавать решение с нуля в продакшене редко бывает необходимо.

Что такое виртуальная прокрутка (и почему это не бесконечная прокрутка)?

Виртуальная прокрутка (также называемая виртуализацией списков или оконным рендерингом) отображает только элементы, видимые в данный момент во viewport, плюс небольшой буфер сверху и снизу. По мере прокрутки элементы, покидающие viewport, удаляются из DOM, а на их место вставляются новые. Полный набор данных никогда не попадает в DOM целиком.

Это принципиально отличается от бесконечной прокрутки (infinite scroll). Бесконечная прокрутка добавляет элементы в DOM по мере прокрутки — список продолжает расти. Виртуальная прокрутка заменяет элементы, сохраняя количество DOM-узлов примерно постоянным независимо от размера набора данных.

Практическая разница существенна. Наивно отрендеренный список из 100 000 элементов может создать 100 000+ DOM-узлов в памяти. Виртуализированный список того же набора данных может содержать 50–80 узлов в любой момент времени.

Как работают виртуализированные списки концептуально

Механизм основан на нескольких простых идеях, работающих вместе:

Окно viewport. Вы задаёте контейнеру прокрутки фиксированную высоту. Это определяет, сколько элементов видно одновременно — назовём это visibleCount = Math.ceil(containerHeight / itemHeight).

Вычисление индексов. По мере прокрутки вы читаете scrollTop, чтобы определить, какой элемент находится в верхней части видимой области: startIndex = Math.floor(scrollTop / itemHeight). Конечный индекс следует за ним: endIndex = startIndex + visibleCount.

Иллюзия позиции прокрутки. Если вы рендерите только 50 элементов, полоса прокрутки будет отражать крошечный список. Чтобы имитировать полную высоту, вы размещаете пустой элемент-заполнитель над отрендеренными элементами (высота = startIndex × itemHeight) и ещё один снизу (высота = оставшееся пространство). Полоса прокрутки ведёт себя так, как будто присутствует весь набор данных.

Overscan (буфер). Рендеринг только точно видимых элементов вызывает резкий эффект появления при быстрой прокрутке. Overscan рендерит несколько дополнительных строк выше и ниже viewport — обычно 5–10 элементов, в зависимости от случая использования — чтобы элементы уже находились в DOM до того, как попадут в поле зрения.

Фиксированная vs. динамическая высота элементов

Виртуализация с фиксированной высотой проста и надёжна. Все вычисления — это простая арифметика.

Динамическая высота значительно сложнее. Вам нужно либо измерять каждый элемент после рендеринга и кэшировать эти измерения, либо предварительно оценивать высоту и корректировать её после измерения. Оба подхода добавляют сложность и могут вызвать нестабильность позиции прокрутки, если не обработаны аккуратно. Если ваш случай использования позволяет, стоит проектировать решение с фиксированной высотой.

Реальные компромиссы, которых стоит ожидать

Виртуальная прокрутка не бесплатна. Некоторые вещи ломаются или требуют дополнительной работы:

  • Браузерный текстовый поиск (Ctrl+F) перестаёт работать надёжно, потому что большая часть контента отсутствует в DOM. Вам потребуется реализовать собственный поиск.
  • Доступность требует внимания. Применяйте role="list", role="feed" или role="grid" к контейнеру. Можно использовать атрибуты вроде aria-setsize и aria-posinset, чтобы вспомогательные технологии могли понять полный размер списка и позицию каждого элемента. Поддерживайте управление фокусом, чтобы клавиатурная навигация не ломалась при размонтировании элементов. Небольшой буфер overscan также помогает программам чтения с экрана обнаруживать наличие дополнительного контента.
  • Стабильность позиции прокрутки становится сложной задачей при динамическом обновлении данных — элементы, добавленные или удалённые выше текущей позиции прокрутки, могут вызывать резкие скачки.

Поддержка в экосистеме фреймворков

В продакшене редко нужно создавать это с нуля. Зрелые библиотеки обрабатывают граничные случаи:

  • React: TanStack Virtual (headless, гибкая) и react-window (лёгкая, поддерживает фиксированные и переменные размеры с дополнительной настройкой)
  • Angular: CDK Virtual Scroll встроена в Angular Component Dev Kit
  • Vue: vue-virtual-scroller покрывает большинство распространённых паттернов

Одна CSS-альтернатива, о которой стоит знать: content-visibility: auto позволяет браузеру пропускать рендеринг контента вне экрана без JavaScript. Это может улучшить производительность отрисовки на списках средней длины, но не уменьшает количество DOM-узлов и не заменяет полную виртуализацию для больших наборов данных.

Когда действительно стоит использовать

Виртуальная прокрутка добавляет сложность. Она оправдана, когда:

  • Ваш список превышает несколько сотен элементов, и производительность прокрутки заметно ухудшается
  • Вы создаёте таблицы, просмотрщики логов, ленты или интерфейсы в стиле электронных таблиц
  • Потребление памяти является ограничением (мобильные устройства, долгоживущие сессии)

Для коротких списков пагинация или простая ленивая загрузка часто проще и достаточно хороши.

Заключение

Пользователям не нужны 100 000 DOM-узлов — им нужно ощущение, что они могут прокручивать 100 000 элементов. Виртуальная прокрутка обеспечивает это ощущение при малой доле затрат на рендеринг. Рендеря только видимую часть набора данных и заменяя элементы по мере прокрутки пользователя, вы сохраняете количество DOM-узлов низким, потребление памяти предсказуемым, а частоту кадров плавной. Компромиссы — сломанный Ctrl+F, вопросы доступности, управление позицией прокрутки — реальны, но хорошо изучены, а экосистема библиотек для React, Angular и Vue обрабатывает большинство из них из коробки. Если ваш список достаточно велик, чтобы ухудшать производительность, виртуализация — самый эффективный доступный инструмент.

Часто задаваемые вопросы

Да, но это требует дополнительной осторожности. Большинство библиотек виртуализации предполагают макет списка в один столбец. Для макетов на основе сетки нужно вычислять видимые строки и столбцы вместе, учитывая количество элементов в строке. TanStack Virtual поддерживает виртуализацию сетки нативно. С другими библиотеками может потребоваться рассматривать каждую строку как один виртуализированный элемент, содержащий несколько ячеек.

Поисковые роботы обычно не прокручивают контент, поэтому элементы вне начального рендеринга не будут проиндексированы. Если SEO важно для контента вашего списка, рассмотрите пагинированный HTML-вывод или альтернативы, дружественные к роботам. Если вы рендерите контент на сервере, применяйте виртуализацию только после гидратации на клиенте.

Обычно это означает, что ваш буфер overscan слишком мал или рендеринг элементов слишком медленный. Увеличьте количество overscan, чтобы больше элементов вне экрана рендерились заранее. Также проверьте, не вызывают ли элементы списка дорогостоящие пересчёты компоновки или синхронную загрузку изображений. Упрощение компонентов элементов и использование контента-заполнителя для изображений может значительно сократить количество пустых кадров.

Да. Перед навигацией сохраните текущее значение scrollTop и соответствующий startIndex в состояние или session storage. Когда пользователь возвращается, восстановите позицию контейнера прокрутки до сохранённого значения scrollTop. Большинство библиотек виртуализации предоставляют метод scrollToIndex или scrollToOffset, что делает это простым в реализации при повторном монтировании.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay