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

Перерисовки в интерфейсных компонентах на основе DOM часто становятся узким местом при работе с большими наборами данных, и Tom Select не является исключением: каждая операция обновления списка опций, значений или состояния контролов может приводить к повторному построению DOM-узлов, пересчёту лэйаута и дорогостоящим repaint/reflow.

Внутренне Tom Select опирается на несколько ключевых слоёв:

  • состояние данных (options, items, value)
  • представление (dropdown, control, option elements)
  • рендер-функции (render, renderItem, renderOption)
  • механизмы синхронизации DOM (refreshOptions, updateOptions, setValue)

Каждое изменение состояния потенциально триггерит цепочку обновлений:

setValue → sync → refreshItems → refreshOptions → renderDropdown

При неоптимальном использовании API эти цепочки становятся причиной избыточных перерисовок.


Избыточные обновления при setValue

Одна из самых частых проблем — множественные вызовы установки значения:

select.setValue('a');
select.setValue('b');
select.setValue('c');

Каждый вызов приводит к:

  • обновлению внутреннего массива items
  • перерисовке выбранных элементов
  • обновлению input control
  • возможному пересчёту dropdown

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

Tom Select позволяет подавлять лишние события:

select.setValue(['a', 'b', 'c'], true);

Флаг silent уменьшает количество побочных эффектов и предотвращает каскадные обновления UI.


Контроль перерисовки dropdown

Dropdown — самая дорогая часть интерфейса, особенно при большом числе опций.

Проблема полного пересоздания списка

Метод refreshOptions при каждом изменении может:

  • очищать контейнер
  • заново создавать option nodes
  • повторно применять фильтры

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

Ограничение количества отображаемых элементов

Использование конфигурации:

{
  maxOptions: 50
}

снижает нагрузку, уменьшая DOM-дерево до фиксированного размера, что критично для производительности.


Кастомный рендер как источник лишних перерисовок

Функции renderOption и renderItem вызываются при каждом обновлении состояния.

render: {
  option: function(data, escape) {
    return `<div>${escape(data.text)}</div>`;
  }
}

Проблема создания новых строк DOM

При каждом refreshOptions создаются новые HTML-строки, даже если данные не изменились.

Подход к стабилизации рендера

Ключевой принцип — детерминированный рендер без побочных эффектов:

  • не использовать случайные значения
  • не генерировать динамические id
  • избегать вычислений внутри render-функций

Дополнительно можно кэшировать результат:

const cache = new Map();

render: {
  option: function(data, escape) {
    if (cache.has(data.value)) return cache.get(data.value);

    const html = `<div>${escape(data.text)}</div>`;
    cache.set(data.value, html);
    return html;
  }
}

Управление вводом и debounce

Поиск через input вызывает частые обновления dropdown:

input event → load → refreshOptions → render

Без ограничения частоты запросов возникает серия перерисовок.

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

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

Применение:

select.on('type', debounce(function(query) {
  select.load(function(callback) {
    fetch('/api?q=' + query)
      .then(r => r.json())
      .then(callback);
  });
}, 200));

Это снижает частоту refreshOptions и уменьшает DOM churn.


Минимизация refreshOptions

refreshOptions часто вызывается при:

  • изменении search query
  • изменении options
  • открытии dropdown

Проблема повторной фильтрации

При каждом вызове происходит:

  • пересчёт score (если включён sorting)
  • фильтрация массива
  • рендер списка

Подход: предварительная нормализация данных

Если данные статичны, фильтрацию можно вынести наружу:

const precomputed = data.map(item => ({
  ...item,
  searchIndex: item.text.toLowerCase()
}));

И отключить лишнюю логику сортировки.


Избегание полного пересоздания инстанса

Частая ошибка — уничтожение и создание нового Tom Select при каждом изменении данных.

select.destroy();
select = new TomSelect('#select', config);

Это приводит к:

  • полной перестройке DOM
  • повторной инициализации событий
  • потере состояния UI

Гораздо дешевле обновлять данные:

select.clearOptions();
select.addOptions(newOptions);
select.refreshOptions(false);

Оптимизация работы с большим количеством опций

При тысячах элементов критично:

  • избегать вложенных DOM структур
  • минимизировать HTML внутри option
  • отключать ненужные плагины

Упрощение DOM-структуры

Чем проще шаблон option, тем быстрее repaint:

render: {
  option: (data, escape) => `<div>${escape(data.text)}</div>`
}

Асинхронная загрузка и контроль перерисовок

loadCallback может вызываться многократно при быстром вводе.

Проблема: ответы приходят не по порядку.

Решение через идентификатор запроса

let requestId = 0;

select.load(function(query, callback) {
  const id = ++requestId;

  fetch('/api?q=' + query)
    .then(r => r.json())
    .then(data => {
      if (id !== requestId) return;
      callback(data);
    });
});

Это предотвращает лишние refreshOptions от устаревших запросов.


Минимизация repaint через управление состоянием открытого dropdown

Каждое открытие dropdown может вызывать пересборку списка.

Оптимизация:

  • избегать закрытия/открытия при каждом изменении
  • использовать silent обновления перед открытием
select.setValue(value, true);
select.open();

Снижение нагрузки через отключение лишних обновлений input

Input внутри control также участвует в синхронизации состояния.

При массовых обновлениях:

select.setValue(values, true);
select.blur();

можно снизить количество перерасчётов фокуса и layout shifts.


Контроль частоты обновлений через requestAnimationFrame

Некоторые операции лучше выносить в следующий кадр:

function scheduleUpdate(fn) {
  requestAnimationFrame(() => fn());
}

Применение:

  • batch обновление options
  • массовое добавление items
  • синхронизация UI после загрузки данных

Изоляция дорогих операций рендера

Tom Select не имеет встроенного виртуального списка, поэтому при больших данных критично:

  • ограничивать dataset на уровне сервера
  • использовать пагинацию
  • подгружать данные по мере ввода

Фильтрация на клиенте должна рассматриваться как исключение, а не основная стратегия.


Стабилизация состояния при частых изменениях

При интенсивных обновлениях важно избегать цепных реакций:

  • setValue не должен триггерить reload
  • refreshOptions не должен менять value
  • load не должен вызывать повторный load

Подход — разделение потоков:

  • data layer (options)
  • UI layer (render)
  • state layer (value)

Чёткое разделение уменьшает количество каскадных перерисовок и стабилизирует поведение компонента при высокой частоте изменений.