15 авг. 2025 г.·8 мин чтения

Библиотеки виртуализации React для быстрых длинных списков

Библиотеки виртуализации React помогают длинным спискам работать плавно даже на слабых ноутбуках. Узнайте, где windowing приносит наибольшую пользу в таблицах, деревьях и результатах поиска.

Библиотеки виртуализации React для быстрых длинных списков

Почему длинные списки кажутся медленными

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

Часто сначала винят React, но обычно большая часть нагрузки приходится именно на браузер. Страница с 5000 строк может выглядеть просто, но в каждой строке легко оказываются несколько элементов. В итоге один список превращается в десятки тысяч объектов, за которыми браузеру нужно следить во время прокрутки.

Прокрутка добавляет ещё одну нагрузку. Многие приложения обновляют состояния наведения, лениво подгружают изображения, закрепляют заголовки, показывают поля выделения или детали строки, пока пользователь крутит колесо. Если каждый шаг скролла вызывает обновления React в большом дереве компонентов, кадры начинают пропадать. Это ощущается как рывки, задержка кликов или список, который на долю секунды залипает.

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

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

Именно поэтому библиотеки виртуализации React так важны. Они уменьшают объём работы, оставляя на странице только видимые строки и небольшой буфер. Если человек видит 20 элементов, рендерить 2000 обычно просто пустая трата.

Где виртуализация даёт наибольший эффект

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

Именно здесь библиотеки виртуализации React особенно уместны. Они держат в DOM только небольшой фрагмент элементов и подменяют их по мере прокрутки. Если на экране помещается 25 строк, приложение может рендерить 30 или 40, а не все 5000.

Наибольший выигрыш обычно виден в таких местах:

  • таблицы данных с большим количеством похожих строк
  • результаты поиска, которые пользователи быстро просматривают
  • журналы событий или ленты активности, которые постоянно растут
  • деревья с большим числом раскрытых узлов

Повторяющиеся строки выигрывают первыми, потому что их макет предсказуем. Хороший пример — таблица с простыми ячейками. Большинство пользователей видят одновременно лишь несколько десятков строк, так что отрисовка тысяч скрытых строк не даёт им никакой пользы и только мешает плавной прокрутке.

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

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

Короткие списки — совсем другая история. Если на странице 20 или 40 элементов, обычный рендеринг обычно проще и уже достаточно быстрый. Виртуализация добавляет настройку, сложности с измерением и странности с фокусом, так что оправдать её трудно, если пользователи не прокручивают список далеко.

Выберите библиотеку под задачу

Неудачный выбор быстро проявляется, когда список становится большим. Почти любую библиотеку можно заставить работать, но слабые ноутбуки особенно остро реагируют на неправильное решение: появляются рывки, дерганая прокрутка или пропадающий фокус.

Среди библиотек виртуализации React часто лучше всего работает самый простой вариант. Если у каждой строки одинаковая высота, выбирайте react-window. Он небольшой, делает одну задачу хорошо и не перегружает голову лишним.

Список товаров — хороший пример. Если высота каждой строки 48px и макет не меняется, react-window обычно вполне достаточно. Вы получаете плавную прокрутку без лишних возможностей, которыми, возможно, никогда не воспользуетесь.

Если высота строк сильно меняется, с React Virtuoso жить проще. Он лучше справляется с сообщениями, комментариями, карточками поиска и другим смешанным контентом, потому что сам обрабатывает изменения размера. Это экономит время и помогает избежать неудобных пустот и резких сдвигов, которые часто появляются, когда высота только угадывается.

TanStack Virtual подходит командам, которым нужен больший контроль. Он хорошо работает, когда вы строите таблицу, сетку или дерево со своей структурой и хотите тонко настраивать рендер и измерение элементов. Настройка там сложнее, зато вы можете сами задавать поведение, а не подстраиваться под жёсткий шаблон.

Простой способ выбрать:

  • Используйте react-window для обычных списков со строками фиксированной высоты.
  • Используйте React Virtuoso, когда высота строк часто меняется.
  • Используйте TanStack Virtual, когда нужна таблица, сетка или дерево с более точным контролем.

Не останавливайтесь только на скорости прокрутки. Перед тем как принять решение, проверьте sticky-заголовки, клавиатурный фокус и overscan.

Sticky-заголовки важны в таблицах, потому что пользователи быстро теряют контекст, когда названия колонок исчезают. Клавиатурный фокус важен в результатах поиска и деревьях, где люди перемещаются стрелками, Tab или Enter. Overscan важен потому, что несколько дополнительных строк могут сделать быструю прокрутку плавнее, но слишком большой overscan снова создаёт тяжёлый DOM, от которого вы пытались уйти.

Если сомневаетесь, сначала соберите самый маленький реальный экран. Список фиксированной высоты, таблица со sticky-заголовком или дерево с раскрытием и сворачиванием скажут вам больше за 20 минут, чем длинная таблица возможностей.

Как добавить виртуализацию шаг за шагом

Начинайте с области просмотра, а не с данных. Измерьте высоту контейнера списка, а затем прикиньте, сколько строк помещается на экран. Если панель показывает около 12 элементов, вам не нужно рендерить 2000. Вам нужны 12 плюс небольшой запас, чтобы при прокрутке не мелькали пустые места.

Обычно простая настройка выглядит так:

  1. Измерьте высоту контейнера списка.
  2. Задайте высоту строки или близкую оценку.
  3. Рендерьте только видимые строки плюс несколько сверху и снизу.
  4. Держите компоненты строк небольшими и с устойчивыми props.
  5. Подгружайте следующую порцию до того, как пользователь дойдёт до конца.

Этот буфер важен сильнее, чем многие думают. Слишком маленький — и список кажется дёрганым, когда человек быстро прокручивает. Слишком большой — и вы теряете выгоду от windowing в React, потому что браузеру всё равно нужно отрисовывать слишком много узлов. На старом ноутбуке даже разница в 20 или 30 лишних строк может заметно изменить ощущение плавности.

Делайте каждую строку простой. Строка должна в основном показывать данные, обрабатывать клик и на этом останавливаться. Сложное форматирование, глубокие вложенные компоненты и inline-функции в каждом элементе добавляют стоимость на каждый скролл. Если в строке долго показываются один и тот же аватар, подпись или статус, мемоизируйте этот контент, чтобы React не собирал его заново.

Стабильная идентичность тоже помогает. Используйте реальный id элемента, а не индекс массива, если список может меняться местами или подгружать новые результаты. Так выделение, состояния наведения и раскрытые детали не будут перескакивать на неправильную строку.

Для загрузки не ждите, пока появится последний элемент. Запускайте следующий запрос чуть раньше — например, когда пользователь приблизится к концу на один-два экрана. Экран поиска с 5000 результатами ощущается намного быстрее, если новые строки уже готовы до того, как пользователь заметит границу.

Большинство библиотек виртуализации React берут математику на себя. Ваша задача проще: измерить видимую область, сделать строки дешёвыми для рендера и убедиться, что новые данные приходят до того, как список кажется пустым.

Что меняется в таблицах

Нужен технический партнёр
Привлеките Oleg, когда стартапу нужна архитектура продукта и практическая помощь CTO.

Таблица ломается легче, чем обычный список. Здесь важны не только строки, но и выравнивание колонок, закреплённые части и поведение ячеек.

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

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

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

Рендер ячеек быстро становится дорогим

Большая часть тормозов в таблице идёт именно от ячеек, а не от строк как таковых. Строка с 12 колонками — это 12 путей рендера, 12 расчётов стилей и часто 12 областей событий.

Стоимость растёт очень быстро. Если одновременно видно 40 строк, а в каждой по 15 колонок, браузер обрабатывает 600 ячеек. Добавьте иконки, выпадающие меню, форматирование дат, чипы статуса и встроенные графики — и прокрутка начинает ощущаться тяжёлой.

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

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

Короткий тест помогает поймать большинство ошибок в таблицах:

  • Прокрутите таблицу глубоко вниз, а затем отсортируйте колонку
  • Измените ширину нескольких колонок до очень узкой и очень широкой
  • Закрепите и открепите колонку во время горизонтальной прокрутки
  • Пройдитесь по ячейкам с клавиатуры, выделите текст и скопируйте значение

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

Что меняется в деревьях

Дерево сложнее простого списка, потому что каждая строка зависит от глубины и состояния раскрытия. С React tree virtualization вы перестаёте рендерить всю вложенную структуру. Вы показываете плоский список только тех узлов, которые видны прямо сейчас.

Это звучит просто, но меняет весь подход. Если папка закрыта, её дочерних элементов вообще не должно быть в рендеримом списке. Если папка раскрывается, добавляйте только эту ветку в плоский список, а не перестраивайте каждую видимую строку заново.

Держите видимый фрагмент маленьким

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

Полная перестройка при каждом раскрытии или сворачивании всё равно может ощущаться медленной на слабом ноутбуке. Лучше хранить карту id узлов, глубины и состояния родителя, а затем обновлять только затронутый диапазон. Здесь особенно важны стабильные id. Они помогают React переиспользовать строки, а не выбрасывать их и рисовать заново.

Отступы тоже требуют внимания. Пользователи считывают глубину дерева по расстоянию слева, поэтому у каждой строки должен быть понятный левый отступ или линия-ориентир, соответствующая уровню. Не имитируйте глубину так, чтобы она менялась при переиспользовании строк, иначе дерево будет выглядеть так, будто оно прыгает при прокрутке.

Ввод и порядок чтения

Клавиатура важнее в деревьях, чем в простых списках. Люди ожидают, что стрелки будут открывать, закрывать и перемещать между элементами в предсказуемом порядке. Порядок в DOM должен совпадать с визуальным порядком, иначе скринридеры будут озвучивать всё неправильно, даже если дерево выглядит нормально.

Несколько проверок ловят большую часть проблем:

  • Стрелка вправо открывает закрытого родителя или переводит к его первому дочернему элементу
  • Стрелка влево закрывает открытого родителя или переводит к родителю
  • Фокус по Tab остаётся на активном элементе, а не на каждой строке
  • Состояние раскрытия и уровень остаются правильными, когда строки переиспользуются
  • Строки с переносом текста сохраняют правильную высоту после прокрутки туда и обратно

Перенос строк создаёт ещё одну проблему: высоту. Если названия некоторых узлов занимают две строки, кэшируйте эти высоты после первого измерения. Без кэша строки сдвигаются во время прокрутки, и дерево начинает казаться шатким. Пользователи замечают такой баг сразу, даже если не могут его описать.

Результаты поиска на старом ноутбуке

Спланируйте более безопасный запуск
Проверьте изменения в виртуализации до того, как они сломают фокус, выделение или состояния загрузки.

Поиск — это место, где длинные списки начинают реально мешать. Человек вводит три буквы, получает 8000 совпадений и ожидает, что страница будет успевать за ним, пока он продолжает печатать. На слабом ноутбуке замедление часто начинается в тот момент, когда приложение пытается смонтировать сразу все строки результатов.

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

Виртуализация сокращает эту работу до той части, которую люди действительно видят. Вместо рендера всего набора она оставляет на экране небольшой фрагмент и немного запаса сверху и снизу. Список по-прежнему выглядит полным, но браузер рисует, возможно, 30 строк вместо 8000.

Это помогает не только при прокрутке, но и во время ввода. React обновляет меньше узлов после каждого изменения запроса, поэтому поле ввода остаётся отзывчивым. На старом ноутбуке эту разницу легко почувствовать.

Но строки всё равно должны быть простыми. Виртуализация не спасёт результат, если он набит тяжёлой разметкой, вложенными popover-ами и большими изображениями.

  • Показывайте только те поля, которые люди смотрят в первую очередь
  • Ограничьте число видимых тегов и прячьте остальные под «ещё»
  • Держите миниатюры маленькими или уберите их совсем
  • Перенесите дополнительные действия в панель деталей

Помогает и debounce при поиске. Подождать примерно 150–250 мс перед запуском нового запроса часто приятнее, чем отправлять поиск на каждый символ. Страница делает меньше работы, а пользователь видит меньше мерцаний, пока обновляются фильтры и счётчики.

Хорошая строка результата обычно хороша именно своей простотой. Название, цена, статус и несколько тегов — этого достаточно для списка. Плотный макет оставьте для страницы элемента или боковой панели, и поиск останется быстрым даже на старом железе.

Ошибки, из-за которых прокрутка дёргается

Виртуализированный список всё равно может ощущаться плохо, если строки продолжают меняться уже после того, как React их отрисовал. Самая частая проблема — высота. Если строка таблицы растёт, когда загружается аватар, переносится бейдж или узел дерева раскрывается с неизвестным содержимым, положение прокрутки смещается, и пользователь чувствует рывок.

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

Использование индекса массива в качестве React key создаёт другую проблему. Отсортируйте таблицу, отфильтруйте результаты поиска или вставьте один элемент наверх — и React может переиспользовать не ту строку. Чекбоксы съезжают, раскрытые ветки цепляются за неправильный узел дерева, и список начинает казаться сломанным. Вместо этого используйте стабильный id из ваших данных.

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

Несколько привычек помогают избежать большей части дёрганья:

  • Делайте разметку строки компактной и откладывайте тяжёлые виджеты до клика пользователя.
  • Подгружайте следующую порцию данных до того, как пользователь дойдёт до края окна.
  • Делайте debounce фильтрации, чтобы список не перестраивался на каждый символ.
  • Мемоизируйте компоненты строк, если их props редко меняются.

Поздняя загрузка данных создаёт ещё один неприятный эффект: пустые пропуски. Заглушки обычно выглядят лучше, чем пустое пространство, особенно в виртуализированных таблицах React, где пользователи ожидают, что сетка останется стабильной.

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

Быстрая проверка перед релизом

Уменьшите лишнюю нагрузку во фронтенде
Сократите тяжёлую разметку строк, лишние перерендеры и скрытый DOM, который нагружает слабые машины.

Список может идеально работать на вашем ноутбуке и всё равно развалиться на старом офисном компьютере. Проверьте сложные сценарии до релиза, потому что библиотеки виртуализации React решают только часть задачи. Тяжёлое содержимое строк, плохие измерения и слишком частые перерендеры всё ещё могут сделать интерфейс медленным.

Если можно, используйте одно слабое устройство. Старый ноутбук на Windows с кучей открытых вкладок браузера часто уже достаточно хорошо показывает проблему.

  • Прокрутите список сверху вниз, а потом обратно вверх. Сделайте это медленно, а потом резко прокрутите колесом или трекпадом. Следите за рывками, поздним появлением строк, запаздывающими sticky-заголовками или таблицей, которая теряет позицию.
  • Несколько раз измените размер окна. Сделайте его узким, широким, низким и высоким. Пустые промежутки, наложение строк или дерево, которое схлопывается в пустоту, обычно означают ошибку в расчёте размеров.
  • Открывайте меню строк, выделяйте элементы с клавиатуры и продолжайте двигаться по списку. Обводка фокуса должна оставаться видимой, а активный элемент не должен исчезать только потому, что виртуализатор переиспользовал этот участок экрана.
  • Принудительно проверьте все поддерживаемые состояния. Показатели загрузки, пустого списка и ошибки должны отображаться в том же контейнере, где появляются реальные результаты. Если макет прыгает между состояниями, люди это почувствуют, даже если не смогут объяснить почему.
  • Вводите запрос в поиск, пока список прокручивается. Откройте диспетчер задач браузера или инструменты производительности и смотрите на загрузку CPU. Если один короткий запрос резко повышает нагрузку, а ввод начинает тормозить, значит фильтрация, подсветка или рендер строк всё ещё делают слишком много.

Небольшой пример: страница поиска может прокручиваться плавно, пока вы не добавите подсветку совпадений, аватары и счётчик результатов в каждой строке. Тогда один символ может подвесить старый ноутбук на полсекунды. Обычно это значит, что окно списка в порядке, но каждая видимая строка всё ещё слишком дорогая.

Если вы нашли проблемы, сначала исправьте самую очевидную. Мемозируйте компоненты строк, уменьшите работу во время ввода и сделайте высоту строк предсказуемой. Быстрый список должен оставаться спокойным, даже когда устройство устало.

Что делать дальше

Начните с экрана, который болит сильнее всего. Если один вид заставляет слабый ноутбук заикаться, чините именно его, а не пытайтесь виртуализировать все списки в приложении сразу.

Хорошо работает простой порядок:

  • Выберите самый медленный экран, например страницу результатов поиска с сотнями строк.
  • Замерьте его до изменений. Посмотрите время рендера, плавность прокрутки и пики CPU.
  • Добавьте windowing только в этот экран и снова сделайте замер на тех же данных.
  • Сначала покажите это небольшой группе пользователей, а затем следите за багами в выделении, клавиатурном вводе и состояниях загрузки.
  • Переходите к следующему экрану только после того, как первый запуск выглядит стабильным.

Так работа остаётся небольшой, а результат легко оценить. Если страница падает с рваных 2 секунд тяжёлого рендера до плавной первой прокрутки, команда увидит выигрыш сразу.

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

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

Если вашей команде нужна более широкая помощь, Oleg Sotnikov может практично разобрать архитектуру, производительность интерфейса и AI-first продуктовую работу. Он работает как Fractional CTO и startup advisor, а его опыт охватывает и архитектуру продукта, и lean production systems. Это подходит командам, которым нужен не просто быстрый патч, а план, который можно продолжать использовать и после запуска первого быстрого списка.