В интерфейсах, содержащих большое количество элементов сетки, возникают проблемы производительности. Когда в DOM одновременно присутствуют сотни или тысячи виджетов, браузер тратит значительные ресурсы на:
Библиотека Gridstack.js поддерживает механизмы оптимизации, позволяющие работать с большими наборами элементов. Одним из ключевых подходов является виртуализация сетки — стратегия, при которой в DOM присутствуют только видимые или близкие к видимой области элементы.
Виртуализация широко применяется в таблицах данных, списках сообщений, панелях аналитики и других интерфейсах, где количество компонентов может достигать нескольких тысяч.
Основная идея:
рендерить только ту часть сетки, которая реально отображается на экране, а остальные элементы хранить в памяти в виде данных.
Рассмотрим типичную ситуацию:
Простейшая инициализация Gridstack может выглядеть так:
const grid = GridStack.init();
for (let i = 0; i < 1000; i++) {
grid.addWidget({
x: i % 12,
y: Math.floor(i / 12),
w: 1,
h: 1,
content: `Widget ${i}`
});
}
В этом случае:
Результат:
Виртуализация решает эту проблему.
Виртуализированная сетка разделяет данные и отображение.
Структура состоит из трёх уровней:
1. Модель данных
Хранит полную информацию о виджетах.
const widgets = [
{id: 1, x:0, y:0, w:2, h:1, content:"A"},
{id: 2, x:2, y:0, w:2, h:1, content:"B"},
...
];
2. Видимая область
Определяется текущим положением прокрутки.
scrollTop
viewportHeight
3. Активные DOM-элементы
В DOM присутствуют только элементы, попадающие в область:
visibleRange
Для расчёта используется положение прокрутки контейнера.
Основные параметры:
scrollTop
viewportHeight
cellHeight
Формула:
firstVisibleRow = floor(scrollTop / cellHeight)
lastVisibleRow = ceil((scrollTop + viewportHeight) / cellHeight)
К этим значениям добавляется буфер, предотвращающий подгрузку при каждом пикселе прокрутки.
Пример:
const buffer = 5;
const startRow = firstVisibleRow - buffer;
const endRow = lastVisibleRow + buffer;
Теперь можно определить элементы, которые должны быть отображены.
Каждый элемент сетки имеет координату y.
Фильтрация:
function getVisibleWidgets(widgets, startRow, endRow) {
return widgets.filter(w => {
return w.y + w.h >= startRow && w.y <= endRow;
});
}
Таким образом в DOM попадёт лишь небольшой поднабор элементов.
Типичный процесс рендеринга состоит из нескольких этапов.
const visibleWidgets = getVisibleWidgets(
widgets,
startRow,
endRow
);
grid.removeAll(false);
false предотвращает уничтожение DOM-контейнера.
visibleWidgets.forEach(w => {
grid.addWidget({
x: w.x,
y: w.y,
w: w.w,
h: w.h,
content: w.content
});
});
Виртуализация напрямую зависит от прокрутки контейнера.
container.addEventListener('scroll', handleScroll);
Функция:
function handleScroll() {
const scrollTop = container.scrollTop;
const viewportHeight = container.clientHeight;
const firstRow = Math.floor(scrollTop / cellHeight);
const lastRow = Math.ceil(
(scrollTop + viewportHeight) / cellHeight
);
updateVisible(firstRow, lastRow);
}
updateVisible обновляет DOM.
Чтобы прокрутка не “прыгала”, высота контейнера должна соответствовать полной высоте сетки.
Общая высота вычисляется:
totalRows * cellHeight
Создаётся виртуальный контейнер.
const spacer = document.createElement('div');
spacer.style.height = totalRows * cellHeight + 'px';
container.appendChild(spacer);
Реальные элементы позиционируются абсолютно.
Виртуализированные элементы не участвуют в обычном потоке документа.
Позиция рассчитывается вручную.
widget.style.position = 'absolute';
widget.style.top = (widgetData.y * cellHeight) + 'px';
widget.style.left = (widgetData.x * cellWidth) + 'px';
Это позволяет:
Без буфера элементы будут появляться с задержкой при прокрутке.
Буфер решает проблему:
visibleRange = viewport + buffer
Пример:
viewport rows: 50
buffer rows: 10
В DOM:
70 строк
Это значительно уменьшает частоту обновлений.
Удаление и повторное создание элементов может быть дорогим.
Поэтому используется пул элементов.
Пример:
const elementPool = [];
При удалении:
function recycleElement(el) {
elementPool.push(el);
}
При создании:
function getElement() {
if (elementPool.length) {
return elementPool.pop();
}
return document.createElement('div');
}
Это снижает нагрузку на GC.
Перетаскивание усложняет виртуализацию.
Проблемы:
Процесс:
grid.on('dragstop', (event, el) => {
const node = el.gridstackNode;
updateWidgetData(node);
});
Resize работает аналогично drag.
После изменения размеров необходимо обновить модель:
grid.on('resizestop', (event, el) => {
const node = el.gridstackNode;
const widget = widgets.find(w => w.id === node.id);
widget.w = node.w;
widget.h = node.h;
});
Даже если элемент отображается, его внутренний контент может быть тяжёлым.
Например:
Lazy-подход:
function renderWidget(widget) {
const el = document.createElement('div');
el.innerHTML = '<div class="loading">...</div>';
requestIdleCallback(() => {
el.innerHTML = widget.content;
});
return el;
}
Современный подход — использование IntersectionObserver.
Он определяет момент появления элемента в области просмотра.
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadWidget(entry.target);
}
});
});
Подключение:
observer.observe(widgetElement);
Scroll-события могут вызываться сотни раз в секунду.
Поэтому используется throttle.
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn(...args);
}
};
}
Использование:
container.addEventListener(
'scroll',
throttle(handleScroll, 50)
);
Более плавный способ — обновление через animation frame.
let ticking = false;
container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
});
Это синхронизирует обновления с рендер-циклом браузера.
Полная модель сетки хранится отдельно от DOM.
Пример структуры:
const gridModel = {
columns: 12,
cellHeight: 80,
widgets: []
};
Каждый элемент:
{
id: 12,
x: 4,
y: 22,
w: 3,
h: 2,
content: "Chart"
}
DOM становится лишь проекцией данных.
Виртуализация особенно полезна в следующих системах:
Сетки с десятками:
Low-code редакторы могут содержать сотни компонентов.
Дашборды с большим количеством метрик.
Модульные страницы с большим количеством блоков.
Несмотря на преимущества, виртуализация имеет ограничения.
Необходима собственная система:
Некоторые элементы могут исчезать при перемещении.
Требуется:
Для огромных сеток используется двухуровневая оптимизация:
Пример:
async function loadWidgets(startRow, endRow) {
const response = await fetch(
`/widgets?start=${startRow}&end=${endRow}`
);
return await response.json();
}
Загружаются только нужные элементы.
Оптимальный размер буфера
5–20 строк
Количество DOM-элементов
Желательно держать:
< 200 элементов
Использование абсолютного позиционирования
Это уменьшает перерасчёт layout.
Минимизация вложенных DOM
Каждый виджет должен быть лёгким.
Отложенная инициализация
Тяжёлые компоненты создаются только при отображении.
Типичная архитектура состоит из нескольких модулей.
GridModel
Хранение данных.
VirtualViewport
Расчёт видимой области.
Renderer
Отрисовка DOM.
WidgetPool
Повторное использование элементов.
ScrollController
Обработка прокрутки.
Такое разделение позволяет масштабировать систему до тысяч элементов, сохраняя плавную работу интерфейса.