Минимизация перерисовок

Shepherd.js — это библиотека для создания интерактивных туров по веб-приложениям. Основной механизм библиотеки строится вокруг шагов (steps), каждый из которых привязан к DOM-элементу или позиционируется относительно окна. Каждый шаг может содержать текстовые подсказки, кнопки управления и кастомные обработчики событий.

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

Связывание шагов с DOM-элементами

Каждый шаг создается через метод tour.addStep() с указанием параметров:

const tour = new Shepherd.Tour({
  defaultStepOptions: {
    scrollTo: true,
    cancelIcon: { enabled: true }
  }
});

tour.addStep({
  id: 'example-step',
  text: 'Пример шага',
  attachTo: { element: '#my-element', on: 'bottom' },
  buttons: [
    {
      text: 'Далее',
      action: tour.next
    }
  ]
});

Ключевой момент: каждый шаг вычисляет свою позицию относительно целевого элемента при вызове метода show(). Если DOM изменяется, библиотека может инициировать повторные расчеты. Это становится критичным при сложных интерфейсах с анимациями и динамическим контентом.

Использование опции scrollTo

Shepherd.js по умолчанию прокручивает страницу до элемента. Опция scrollTo может вызвать дополнительные перерисовки:

defaultStepOptions: {
  scrollTo: false
}

Установка scrollTo: false исключает автоматическую прокрутку, что снижает количество пересчетов позиции шага и ускоряет отображение. При необходимости прокрутка может быть реализована вручную с помощью методов браузера или библиотек вроде IntersectionObserver.

Кэширование целевых элементов

Частая ошибка — каждый шаг заново ищет элемент через селектор document.querySelector(). При динамическом интерфейсе это приводит к множественным обращениям к DOM. Оптимальная стратегия:

const targetElement = document.getElementById('my-element');

tour.addStep({
  id: 'step-cached',
  text: 'Шаг с кэшированным элементом',
  attachTo: { element: targetElement, on: 'top' }
});

Преимущество: позиция шага вычисляется только один раз, если элемент остается в DOM, снижая нагрузку на рендер.

Обновление позиции без полного ререндеринга

Shepherd.js предоставляет метод updateStepOptions() для изменения позиции или текста шага без полной перерисовки:

const step = tour.getById('example-step');

step.updateStepOptions({
  attachTo: { element: '#new-element', on: 'right' }
});

Важно помнить: вызов show() после обновления инициирует пересчет координат. Чтобы минимизировать лишние пересчеты, следует группировать все изменения до одного вызова show().

Дебаунс обновлений при динамических интерфейсах

Для приложений с часто меняющимся контентом полезно использовать дебаунс обновлений:

let updateTimeout;

function updateStepPositionDebounced(step) {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(() => {
    step.show();
  }, 100); // задержка в 100 мс
}

Это предотвращает множественные вызовы show() при анимациях или изменении размеров элементов, минимизируя перерисовки.

Оптимизация через кастомные события

Shepherd.js позволяет использовать when для привязки к событиям:

tour.addStep({
  id: 'step-event',
  text: 'Ожидаем событие',
  when: {
    show: () => console.log('Шаг отображен')
  }
});

Можно использовать собственные события для обновления только при необходимости, например, когда элемент реально появился в DOM:

document.addEventListener('dynamicContentLoaded', () => {
  step.updateStepOptions({ attachTo: { element: '#dynamic-element', on: 'left' } });
});

Такой подход предотвращает ненужные пересчеты позиций при каждом рендере интерфейса.

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

Для многошаговых туров с динамическими изменениями интерфейса рекомендуются следующие практики:

  1. Кэшировать селекторы элементов вместо поиска каждый раз.
  2. Отключить автоматическую прокрутку (scrollTo: false) при ненужной прокрутке.
  3. Группировать изменения шагов перед вызовом show().
  4. Использовать дебаунс или throttle при реактивных обновлениях DOM.
  5. Привязывать шаги к событиям, чтобы пересчет позиции происходил только по необходимости.
  6. Минимизировать использование анимаций в attachTo, так как они инициируют частые пересчеты.

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

Практический пример

const tour = new Shepherd.Tour({ defaultStepOptions: { scrollTo: false } });

const cachedButton = document.querySelector('#start-btn');
tour.addStep({
  id: 'start',
  text: 'Начало тура',
  attachTo: { element: cachedButton, on: 'bottom' },
  buttons: [{ text: 'Далее', action: tour.next }]
});

const dynamicStep = tour.addStep({
  id: 'dynamic',
  text: 'Шаг с динамическим элементом',
  attachTo: { element: null, on: 'top' }
});

document.addEventListener('dynamicLoaded', () => {
  const dynamicElement = document.querySelector('#dynamic-element');
  dynamicStep.updateStepOptions({ attachTo: { element: dynamicElement, on: 'top' } });
});

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