Трендовые github проекты в нашем телеграм канале. Подпишись → Большая сетка не должна рендериться целиком
Адаптивная Grid-разметка удобна для каталогов, галерей, dashboards и карточных интерфейсов. CSS Grid позволяет гибко менять количество колонок, выравнивать элементы и подстраиваться под ширину контейнера. Проблемы начинаются, когда элементов становится не двадцать, а тысячи.
Браузер может быстро отрисовать небольшую сетку. Но если в DOM находятся тысячи карточек с изображениями, кнопками, состояниями и обработчиками событий, интерфейс начинает тормозить. Растёт время layout, страдает scroll, увеличивается потребление памяти, а любое изменение размеров вызывает дорогой пересчёт.
Виртуализация решает эту проблему: в DOM остаются только элементы, которые видны пользователю, плюс небольшой запас. Для списков это привычная техника. Для адаптивного Grid всё сложнее, потому что количество колонок и позиции зависят от ширины контейнера.
Почему обычный virtual list не подходит
В вертикальном списке у каждого элемента есть индекс и примерная высота. Можно быстро определить, какие строки попадают в viewport, и отрисовать только их. Grid добавляет вторую координату: элементы раскладываются по колонкам, а количество колонок меняется при resize.
Если карточки одинаковой высоты, задача относительно простая. Индекс элемента можно преобразовать в строку и колонку:
row = index / columnCount
column = index % columnCount
Но в реальных интерфейсах карточки часто отличаются по высоте: текст разной длины, изображения разных пропорций, дополнительные badges, lazy-loaded контент. Тогда сетка становится ближе к masonry layout, и расчёт видимой области усложняется.
Базовая модель виртуализации
Минимальная схема состоит из нескольких частей:
- Контейнер со scroll.
- Расчёт ширины контейнера.
- Определение количества колонок.
- Оценка высоты строки или позиции элементов.
- Выбор диапазона видимых индексов.
- Абсолютное позиционирование отрисованных карточек.
- Элемент-заполнитель с полной высотой виртуальной сетки.
Пользователь видит обычную прокрутку, но DOM содержит только небольшой фрагмент. Например, при 10 000 карточек можно держать в DOM 60–120 элементов и сохранять плавный scroll.
Адаптивность требует пересчёта
Главная особенность responsive Grid — изменение количества колонок. При ширине 1200 пикселей может быть 4 колонки, при 800 — 3, при 480 — 1 или 2. После изменения ширины прежние координаты элементов становятся неверными.
Поэтому компоненту нужен наблюдатель за размерами контейнера. Обычно используют ResizeObserver, который сообщает о смене ширины. После этого пересчитываются:
- количество колонок;
- ширина карточки;
- позиции элементов;
- общая высота виртуального полотна;
- видимый диапазон.
Важно не запускать тяжёлый пересчёт на каждый пиксель изменения. Resize-события нужно debounce или объединять через requestAnimationFrame, иначе интерфейс будет тормозить при изменении размера окна.
Overscan снижает визуальные рывки
Если отрисовывать строго только viewport, пользователь при быстрой прокрутке может увидеть пустую область до следующего render. Поэтому виртуализаторы используют overscan — запас элементов выше и ниже видимой области.
Для Grid overscan лучше задавать в строках или пикселях, а не только в количестве элементов. При разном числе колонок одинаковое число элементов даёт разный визуальный запас.
Слишком маленький overscan вызывает мерцание. Слишком большой возвращает проблему большого DOM. Обычно значение подбирают экспериментально: интерфейс должен оставаться плавным на слабом устройстве, а DOM — не разрастаться без необходимости.
Динамическая высота карточек
Самая сложная часть — карточки переменной высоты. Есть несколько подходов.
Первый — фиксировать высоту. Это самый быстрый вариант, но он ограничивает дизайн. Хорошо подходит для карточек файлов, товаров или однотипных метрик.
Второй — оценивать высоту заранее. Например, рассчитывать её по известному типу карточки, количеству строк текста или aspect ratio изображения. Ошибка допустима, если после измерения позиция корректируется.
Третий — измерять элементы после render через ResizeObserver и обновлять карту высот. Такой подход гибкий, но требует аккуратности: если каждое измерение вызывает полный layout, можно потерять выгоду виртуализации.
Для masonry-подобной сетки часто используют карту колонок: каждый новый элемент помещается в колонку с минимальной текущей высотой. Тогда нужно хранить top/left для каждого элемента и быстро искать элементы, пересекающие viewport.
Производительность зависит не только от количества DOM-узлов
Виртуализация уменьшает DOM, но не исправляет тяжёлые карточки. Если каждая карточка запускает сложные эффекты, подписки, canvas, тяжёлые изображения и дорогие вычисления, интерфейс всё равно будет тормозить.
Стоит дополнительно оптимизировать:
- lazy loading изображений;
- мемоизацию карточек;
- стабильные ключи элементов;
- отсутствие лишних re-render;
- минимизацию CSS, влияющего на layout;
- обработчики scroll через
requestAnimationFrame; - разделение данных и представления.
Особенно опасны измерения DOM внутри scroll handler. Чтение layout-свойств и запись стилей в одном цикле могут вызвать layout thrashing. Лучше заранее рассчитывать позиции и пакетировать обновления.
Когда виртуализация не нужна
Не стоит виртуализировать всё подряд. Если в сетке 100–200 простых элементов, обычный CSS Grid может быть быстрее и проще. Виртуализация добавляет сложность: расчёты, edge cases, фокус, доступность, SEO, восстановление позиции scroll.
Она оправдана, когда элементов много, карточки тяжёлые или интерфейс должен работать на слабых устройствах. Хороший критерий — измерения. Если DevTools показывает дорогой layout, long tasks и большой DOM, виртуализация становится практическим решением.
Итог
Виртуализация адаптивной Grid-разметки — это не просто «virtual list в две колонки». Нужно учитывать ширину контейнера, количество колонок, позиции элементов, overscan, динамическую высоту и стоимость измерений.
При правильной реализации пользователь получает плавную прокрутку даже на больших наборах данных, а приложение не держит в DOM тысячи невидимых карточек. Главное — начинать с простой модели, измерять производительность и усложнять алгоритм только там, где это действительно нужно интерфейсу.