Виртуализация: рендеринг только видимых элементов

При работе с большими наборами данных основное узкое место визуализаций на SVG в D3.js связано не с вычислениями, а с количеством DOM-узлов. Каждая точка, линия или прямоугольник в SVG — это отдельный элемент дерева документа, и браузер вынужден поддерживать стили, пересчёт геометрии, перерисовку и события для каждого узла.

При росте количества элементов до десятков тысяч производительность начинает резко снижаться: увеличивается время reflow и repaint, падает отзывчивость взаимодействий, такие операции как zoom, pan и hover становятся нестабильными.

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


Модель видимой области и индексное окно данных

Виртуализация строится вокруг преобразования данных в «окно отображения».

Пусть есть массив данных data, а экран отображает только диапазон элементов [startIndex, endIndex].

const viewportHeight = 600;
const itemHeight = 20;

let scrollTop = 0;

function getVisibleRange() {
  const startIndex = Math.floor(scrollTop / itemHeight);
  const visibleCount = Math.ceil(viewportHeight / itemHeight);

  return {
    startIndex,
    endIndex: startIndex + visibleCount + 1
  };
}

Здесь используется линейная модель, подходящая для списков и упорядоченных структур. Для двумерных данных применяется разбиение по сетке или пространственным индексам.


Связывание виртуального окна с D3 data join

D3.js опирается на механизм связывания данных с DOM через enter / update / exit. Виртуализация добавляет дополнительный слой: перед передачей данных в join происходит фильтрация по текущему окну.

function render() {
  const { startIndex, endIndex } = getVisibleRange();
  const visibleData = data.slice(startIndex, endIndex);

  const selection = d3.select("svg")
    .selectAll("rect")
    .data(visibleData, d => d.id);

  selection.enter()
    .append("rect")
    .attr("x", 0)
    .attr("height", itemHeight - 1)
    .merge(selection)
    .attr("y", (d, i) => (startIndex + i) * itemHeight)
    .attr("width", d => d.value);

  selection.exit().remove();
}

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


Связь scroll-событий с перерасчётом окна

Основной триггер обновления виртуального окна — прокрутка контейнера. При этом важно минимизировать частоту пересчётов, так как scroll генерирует большое количество событий.

const container = document.querySelector(".scroll-container");

container.addEventListener("scroll", () => {
  scrollTop = container.scrollTop;
  requestAnimationFrame(render);
});

Использование requestAnimationFrame синхронизирует обновление с циклом отрисовки браузера, предотвращая избыточные вычисления.


Стабилизация позиционирования элементов

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

.attr("y", d => d.index * itemHeight)

Контейнер SVG или HTML-слой обычно имеет фиксированную высоту, равную data.length * itemHeight, что создаёт иллюзию полного списка.

d3.select(".spacer")
  .style("height", `${data.length * itemHeight}px`);

Такой «спейсер» сохраняет корректную работу скролла без необходимости рендерить все элементы.


Оптимизация через key-функции и стабильность данных

При изменении видимого окна важно сохранять стабильные идентификаторы. Использование key предотвращает пересоздание элементов при скролле.

.data(visibleData, d => d.id)

Без ключей D3 воспринимает элементы как новые при каждом изменении окна, что приводит к постоянному разрушению и созданию DOM-узлов, снижая эффективность виртуализации.


Буферизация вокруг viewport

Жёсткое ограничение только видимыми элементами приводит к визуальным артефактам при быстром скролле. Решением служит добавление буферной зоны (overscan), расширяющей диапазон рендера.

const buffer = 5;

function getVisibleRange() {
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
  const visibleCount = Math.ceil(viewportHeight / itemHeight);

  return {
    startIndex,
    endIndex: startIndex + visibleCount + buffer
  };
}

Буфер снижает вероятность «пустых кадров» при резких перемещениях.


Виртуализация в двумерных структурах

Для таблиц и матриц используется разбиение по строкам и колонкам. Каждая ось обрабатывается отдельно.

const rowHeight = 24;
const colWidth = 80;

function getVisibleGrid() {
  const startRow = Math.floor(scrollTop / rowHeight);
  const startCol = Math.floor(scrollLeft / colWidth);

  const visibleRows = Math.ceil(viewportHeight / rowHeight);
  const visibleCols = Math.ceil(viewportWidth / colWidth);

  return {
    rowRange: [startRow, startRow + visibleRows],
    colRange: [startCol, startCol + visibleCols]
  };
}

Далее данные фильтруются по двум координатам, формируя подмножество матрицы.


Использование SVG clipPath для ограничения области отрисовки

Виртуализация на уровне данных часто комбинируется с визуальным обрезанием через clipPath. Это позволяет отсеивать визуально выходящие за пределы элементы без удаления их из DOM.

svg.append("clipPath")
  .attr("id", "viewport-clip")
  .append("rect")
  .attr("width", width)
  .attr("height", height);

svg.attr("clip-path", "url(#viewport-clip)");

ClipPath не решает проблему количества элементов, но снижает стоимость отрисовки за пределами видимой области.


Альтернативная модель: canvas вместо SVG

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

D3 при этом применяется только как слой вычислений:

  • масштабирование (d3-scale)
  • обработка данных
  • построение координат
const context = canvas.getContext("2d");

function renderCanvas(visibleData) {
  context.clearRect(0, 0, width, height);

  visibleData.forEach(d => {
    context.fillRect(0, d.index * itemHeight, d.value, itemHeight - 1);
  });
}

Canvas устраняет стоимость DOM, но лишает элементы индивидуальной интерактивности.


Пространственная индексация и ускорение фильтрации

При сложных сценах (scatter plot, карты плотности) линейная фильтрация недостаточна. Используется пространственное разбиение через d3-quadtree.

const tree = d3.quadtree()
  .x(d => d.x)
  .y(d => d.y)
  .addAll(data);

const visiblePoints = [];

tree.visit((node, x0, y0, x1, y1) => {
  if (x1 < viewX0 || x0 > viewX1 || y1 < viewY0 || y0 > viewY1) {
    return true;
  }

  if (node.data) {
    visiblePoints.push(node.data);
  }

  return false;
});

Такая структура позволяет отбрасывать большие группы объектов без проверки каждого элемента.


Интеграция с zoom и pan

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

const zoom = d3.zoom()
  .scaleExtent([1, 10])
  .on("zoom", (event) => {
    const transform = event.transform;

    scrollTop = -transform.y / transform.k;
    render();
  });

При масштабировании окно становится динамическим, а размер шага виртуализации изменяется пропорционально коэффициенту k.


Комбинированная архитектура виртуализации

Эффективная система обычно строится как многослойная модель:

  1. слой данных (полный массив)
  2. слой виртуального окна (фильтрация)
  3. слой рендеринга (SVG или canvas)
  4. слой взаимодействий (scroll, zoom, brush)

Каждый слой независим и обновляется только при изменении соответствующих параметров.


Частотное ограничение и стабилизация перерисовок

При интенсивных потоках данных или непрерывном скролле применяется ограничение частоты обновлений.

function throttleRender() {
  if (!ticking) {
    ticking = true;
    requestAnimationFrame(() => {
      render();
      ticking = false;
    });
  }
}

Это предотвращает накопление очереди рендеров и сохраняет стабильную частоту кадров.


Управление памятью при длительной виртуализации

Даже при ограниченном DOM важно контролировать утечки памяти:

  • удаление неиспользуемых слушателей событий
  • очистка ссылок на DOM-элементы
  • повторное использование объектов при возможности

В D3 это особенно важно при постоянном пересоздании selection в динамических интерфейсах.


Гибридные модели рендеринга

В сложных визуализациях применяется комбинация:

  • SVG для интерактивных элементов (tooltip, highlight)
  • Canvas для фонового слоя данных
  • виртуализация для обоих слоёв

Такой подход позволяет сохранить интерактивность без потери производительности при больших объёмах данных.