Debouncing и throttling

В библиотеке Floating UI для работы с плавающими элементами часто возникает необходимость управлять производительностью при обработке событий, таких как скролл, ресайз или изменение состояния элементов. В этих случаях критически важны техники debouncing и throttling, которые позволяют контролировать частоту вызовов функций, предотвращая избыточные вычисления и улучшая отзывчивость интерфейса.


Debouncing

Debouncing — это метод, при котором функция выполняется только после того, как прошло определённое время с момента последнего вызова. Если событие повторяется до истечения таймера, предыдущий вызов отменяется.

function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}

В контексте Floating UI debouncing полезен для оптимизации позиционирования всплывающих элементов при изменении размеров окна или перемещении целевого элемента. Например:

import { computePosition } fr om '@floating-ui/dom';

const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');

const updatePosition = debounce(() => {
  computePosition(button, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip(), shift({ padding: 5 })],
  }).then(({ x, y }) => {
    Object.assign(tooltip.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
}, 100);

window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition);

В этом примере функция updatePosition вызывается не чаще одного раза каждые 100 мс после окончания событий, что предотвращает чрезмерную нагрузку на браузер.


Throttling

Throttling ограничивает частоту вызова функции, позволяя выполнять её не чаще одного раза в заданный интервал времени. В отличие от debouncing, throttling гарантирует регулярное выполнение функции при постоянном потоке событий.

function throttle(fn, lim it) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      fn.apply(this, args);
    }
  };
}

Для Floating UI throttling эффективен при отслеживании скролла страницы, когда требуется плавное, но ограниченное обновление позиции элементов:

const updateTooltipPosition = throttle(() => {
  computePosition(button, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip(), shift({ padding: 5 })],
  }).then(({ x, y }) => {
    Object.assign(tooltip.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
}, 50);

window.addEventListener('scroll', updateTooltipPosition);

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


Различия и практическое применение

Параметр Debouncing Throttling
Когда выполняется После завершения серии событий Периодически во время серии событий
Идеален для Ввод текста, resize окна, завершение drag Скролл, drag-and-drop, постоянные анимации
Основное преимущество Сокращает ненужные вызовы после серии событий Гарантирует регулярное выполнение функции

В Floating UI debouncing часто применяется для редких изменений, например при ресайзе окна, чтобы не пересчитывать позиции каждый пиксель изменения. Throttling применяется для частых событий, таких как скролл или движение мыши, когда позиция элемента должна обновляться регулярно, но с ограничением частоты вызовов.


Интеграция с Floating UI

Для интеграции debouncing и throttling с Floating UI можно комбинировать их с системой autoUpdate, которая автоматически отслеживает изменения положения и размеров элементов:

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

const cleanup = autoUpdate(button, tooltip, debounce(() => {
  computePosition(button, tooltip, {
    placement: 'right',
    middleware: [offset(8), flip(), shift({ padding: 4 })],
  }).then(({ x, y }) => {
    Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px` });
  });
}, 100));

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


Продвинутые варианты

  1. Комбинированный подход: можно применять throttling для регулярного обновления при движении и debouncing для финальной корректировки позиции после завершения активности.
  2. Использование requestAnimationFrame: в некоторых случаях эффективнее использовать requestAnimationFrame вместо фиксированного интервала, особенно для анимаций и плавного скролла.
  3. Middleware адаптация: Floating UI позволяет создавать собственные middleware для оптимизации вычислений, и debouncing/throttling можно встроить в middleware для управления вычислениями в реальном времени.

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