Оптимизация вычислений

Floating UI — это библиотека для позиционирования всплывающих элементов в веб-приложениях. Основная цель её использования — точное управление положением таких элементов (tooltip, popover, dropdown и др.) относительно целевого элемента. Эффективная работа с библиотекой требует понимания, как минимизировать вычислительные затраты и оптимизировать производительность.

Принцип работы вычислений

Floating UI опирается на модель «middleware», которая выполняет цепочку операций для вычисления позиции элемента. Каждое промежуточное звено (middleware) может изменять положение, учитывать ограничения экрана, смещения, перекрытия и пр. Основные middleware:

  • offset — смещает элемент относительно reference.
  • flip — проверяет доступное пространство и при необходимости меняет сторону.
  • shift — корректирует элемент, чтобы он оставался в видимой области.
  • size — динамически подбирает размеры floating-элемента.

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

Минимизация перерасчётов

  1. Использование autoUpdate только при необходимости Функция autoUpdate(reference, floating, callback) отслеживает изменения размеров и прокрутки, вызывая перерасчёт позиции. В проектах с большим количеством элементов постоянное слежение создаёт нагрузку. Решение:

    import { autoUpdate } from '@floating-ui/dom';
    
    const cleanup = autoUpdate(reference, floating, updatePosition, {
      ancestorResize: false, // отключение слежения за родителями
      animationFrame: true   // вызов обновления в requestAnimationFrame
    });

    Это уменьшает количество лишних пересчётов.

  2. Кеширование размеров и координат Если floating-элемент не меняет свои размеры часто, имеет смысл хранить их в памяти и использовать повторно, избегая вызова getBoundingClientRect на каждом событии.

  3. Отсроченные вычисления через requestAnimationFrame Перерасчёт позиций при скролле или resize лучше делать через requestAnimationFrame, чтобы сгруппировать несколько событий в один вызов:

    let ticking = false;
    
    function onScrollOrResize() {
      if (!ticking) {
        requestAnimationFrame(() => {
          updatePosition();
          ticking = false;
        });
        ticking = true;
      }
    }
    
    window.addEventListener('scroll', onScrollOrResize);
    window.addEventListener('resize', onScrollOrResize);

Селективное применение middleware

Использование всех middleware одновременно повышает точность, но увеличивает вычислительную нагрузку. Рекомендуется:

  • offset применять всегда, если нужен сдвиг.
  • flip использовать только при динамических всплывающих элементах, которые могут пересекать границы viewport.
  • shift актуален, когда floating-элемент может выходить за экран.
  • size стоит включать только при изменении размеров контента или ограничении высоты/ширины.

Пример оптимальной цепочки middleware:

import { computePosition, offset, flip, shift } from '@floating-ui/dom';

computePosition(reference, floating, {
  middleware: [
    offset(10),
    flip(),
    shift({ padding: 5 })
  ]
});

Здесь исключено использование size, если размеры фиксированы.

Динамическое включение middleware

В сложных интерфейсах можно включать middleware только при выполнении определённых условий:

const middleware = [offset(8)];

if (shouldFlip) {
  middleware.push(flip());
}

if (shouldShift) {
  middleware.push(shift({ padding: 4 }));
}

computePosition(reference, floating, { middleware });

Это снижает лишние проверки, особенно при множестве элементов на странице.

Пакетная обработка событий

Вместо отдельного вызова computePosition на каждый floating-элемент лучше группировать их обновления:

const floatingElements = [...document.querySelectorAll('.tooltip')];

function batchUpdate() {
  floatingElements.forEach(({ reference, floating }) => {
    computePosition(reference, floating, { middleware });
  });
}

Комбинирование с requestAnimationFrame или IntersectionObserver дополнительно уменьшает нагрузку.

Минимизация пересчёта размеров DOM

Методы getBoundingClientRect и offsetWidth/Height вызывают layout thrashing, если выполняются многократно. Для оптимизации:

  • Измерять размеры один раз при открытии floating-элемента.
  • Кешировать размеры для повторного использования.
  • Избегать изменений DOM внутри middleware, которые вызывают повторные перерасчёты.

Итоговая стратегия

  • Подключать autoUpdate и middleware выборочно.
  • Кешировать размеры и координаты.
  • Использовать requestAnimationFrame для группировки перерасчётов.
  • Минимизировать количество изменений DOM и вызовов layout.
  • Настраивать middleware динамически в зависимости от условий.

Эти подходы позволяют сохранить высокую производительность даже при работе с большим количеством интерактивных floating-элементов и сложными интерфейсами.