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

Проблема производительности при работе с длинными списками

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

В Carbon Components Svelte для этой цели можно использовать сочетание стандартных Svelte-компонентов с библиотеками виртуализации или собственными решениями на основе <svelte:window> и on:scroll.


Основные подходы к виртуализации

  1. Постраничная подгрузка (pagination) Простейший метод — загружать элементы частями (например, по 50–100 штук). Этот подход снижает нагрузку на DOM, но не обеспечивает плавной прокрутки, так как каждая новая порция данных требует асинхронного запроса или дополнительной обработки.

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


Создание виртуализированного списка в Svelte

Структура данных и вычисление видимой области

<script>
  import { onMount } from 'svelte';

  let items = Array.from({ length: 10000 }, (_, i) => `Элемент ${i + 1}`);
  let start = 0;
  let end = 20; // изначально отображаем 20 элементов
  let viewportHeight = 400; 
  let itemHeight = 40; // фиксированная высота одного элемента

  let visibleItems = items.slice(start, end);

  function handleScroll(event) {
    const scrollTop = event.target.scrollTop;
    start = Math.floor(scrollTop / itemHeight);
    end = start + Math.ceil(viewportHeight / itemHeight);
    visibleItems = items.slice(start, end);
  }
</script>

<div on:scroll={handleScroll} style="height: {viewportHeight}px; overflow-y: auto;">
  <div style="height: {items.length * itemHeight}px; position: relative;">
    {#each visibleItems as item, i (item)}
      <div style="position: absolute; top: {(start + i) * itemHeight}px; height: {itemHeight}px; width: 100%;">
        {item}
      </div>
    {/each}
  </div>
</div>

Ключевые моменты реализации:

  • Контейнер с фиксированной высотой и overflow-y: auto для создания scrollable зоны.
  • Абсолютное позиционирование элементов внутри общего блока для правильного отображения виртуализированного списка.
  • Вычисление индексов start и end на основе текущей позиции скролла.

Интеграция с Carbon Components

Компоненты Carbon, такие как DataTable, можно виртуализировать аналогичным способом. Основное отличие — обработка таблицы и строк через слоты и структуры компонентов Carbon.

Пример виртуализации DataTable:

<script>
  import { DataTable, TableContainer, Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from 'carbon-components-svelte';

  let rows = Array.from({ length: 5000 }, (_, i) => ({ id: i + 1, name: `Элемент ${i + 1}` }));
  let start = 0;
  let end = 20;
  let rowHeight = 50;
  let viewportHeight = 400;
  let visibleRows = rows.slice(start, end);

  function handleScroll(event) {
    const scrollTop = event.target.scrollTop;
    start = Math.floor(scrollTop / rowHeight);
    end = start + Math.ceil(viewportHeight / rowHeight);
    visibleRows = rows.slice(start, end);
  }
</script>

<TableContainer style="height: {viewportHeight}px; overflow-y: auto;" on:scroll={handleScroll}>
  <Table>
    <TableHead>
      <TableRow>
        <TableHeader>ID</TableHeader>
        <TableHeader>Название</TableHeader>
      </TableRow>
    </TableHead>
    <TableBody>
      <div style="position: relative; height: {rows.length * rowHeight}px;">
        {#each visibleRows as row, i (row.id)}
          <TableRow style="position: absolute; top: {(start + i) * rowHeight}px;">
            <TableCell>{row.id}</TableCell>
            <TableCell>{row.name}</TableCell>
          </TableRow>
        {/each}
      </div>
    </TableBody>
  </Table>
</TableContainer>

Особенности:

  • Высота блока TableBody задается общей длиной всех строк, чтобы скролл работал корректно.
  • Абсолютное позиционирование позволяет отображать только видимые строки, сохраняя виртуализацию.
  • Можно динамически изменять количество отображаемых элементов, адаптируя к размеру окна.

Оптимизация и расширения

  1. Буферизация элементов Чтобы избежать «мерцания» при быстрых прокрутках, добавляют небольшой буфер элементов до и после видимой зоны:

    const buffer = 5;
    visibleItems = items.slice(Math.max(0, start - buffer), Math.min(items.length, end + buffer));
  2. Динамическая высота элементов Если элементы имеют разную высоту, нужно хранить массив накопленной высоты и искать видимые элементы бинарным поиском, что сложнее, но обеспечивает корректную прокрутку.

  3. Использование готовых библиотек виртуализации

    • svelte-virtual-list
    • svelte-virtual Эти библиотеки облегчают работу, предоставляя готовые компоненты с оптимизацией производительности.

Рекомендации по производительности

  • Фиксированная высота элементов ускоряет вычисления и упрощает рендеринг.
  • Минимизация количества DOM-операций при скролле — критически важно для плавности.
  • Для DataTable с большим количеством колонок полезно использовать column virtualization совместно с row virtualization.
  • Слежение за изменением размеров окна (on:resize) позволяет корректно пересчитывать количество отображаемых элементов.

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