При увеличении количества элементов до десятков тысяч и более стандартная модель загрузки и фильтрации в браузере начинает деградировать: растёт время первого рендера, увеличивается нагрузка на DOM, поисковая операция становится дорогой, а взаимодействие с интерфейсом теряет отзывчивость. В контексте компонентов выбора это особенно критично, поскольку пользователь ожидает мгновенного отклика при вводе текста и открытии списка.
Tom Select изначально проектировался как легковесная замена
стандартного <select>, но его реальная сила
проявляется именно при грамотной настройке работы с большими наборами
данных через асинхронную подгрузку, серверный поиск и контроль
рендеринга.
Основная стратегия оптимизации заключается в том, чтобы полностью исключить загрузку всего массива данных в браузер.
Вместо:
Array.filterиспользуется модель:
Ключевая настройка:
new TomSelect("#select", {
loadThrottle: 300,
preload: false,
persist: false
});
loadThrottle снижает частоту сетевых запросов,
предотвращая лавинообразный рост запросов при вводе текста.
Центральный механизм масштабирования — функция load,
которая полностью заменяет локальный источник данных.
new TomSelect("#select", {
valueField: "id",
labelField: "title",
searchField: ["title"],
load: function(query, callback) {
if (!query.length) return callback();
fetch(`/api/items?q=${encodeURIComponent(query)}`)
.then(res => res.json())
.then(data => callback(data))
.catch(() => callback());
}
});
Оптимизационные моменты:
Даже с loadThrottle важно учитывать задержки ввода. При
больших данных типичный серверный поиск должен работать с задержкой
200–400 мс.
Дополнительный слой контроля:
let timer;
function debounceLoad(query, callback, loadFn) {
clearTimeout(timer);
timer = setTimeout(() => loadFn(query, callback), 300);
}
Использование в load:
load: function(query, callback) {
debounceLoad(query, callback, serverRequest);
}
Это уменьшает нагрузку на API и снижает вероятность гонки запросов.
При быстром вводе предыдущие запросы могут возвращаться позже новых, ломая актуальность данных.
Решение — контроль токена запроса:
let lastRequestId = 0;
load: function(query, callback) {
const requestId = ++lastRequestId;
fetch(`/api?q=${query}`)
.then(r => r.json())
.then(data => {
if (requestId !== lastRequestId) return;
callback(data);
});
}
Это гарантирует, что отображаются только актуальные данные.
При больших данных многие запросы повторяются (особенно при удалении символов в поиске).
Простейший in-memory cache:
const cache = new Map();
load: function(query, callback) {
if (cache.has(query)) {
callback(cache.get(query));
return;
}
fetch(`/api?q=${query}`)
.then(r => r.json())
.then(data => {
cache.set(query, data);
callback(data);
});
}
Оптимизация:
Даже при серверной фильтрации важно ограничивать размер возвращаемого набора.
Рекомендуемые практики:
На клиенте:
maxOptions: 50
Каждый элемент списка в Tom Select создаёт DOM-ноду. При больших объёмах это становится узким местом.
Ключевые принципы:
render: {
option: function(data, escape) {
return `<div>${escape(data.title)}</div>`;
},
item: function(data, escape) {
return `<div>${escape(data.title)}</div>`;
}
}
Запрещено:
Чем больше полей участвует в поиске, тем дороже операция:
searchField: ["title"]
Плохая практика:
searchField: ["title", "description", "tags", "meta", "category"]
Каждое дополнительное поле увеличивает вычисления и снижает скорость фильтрации.
Встроенный scoring может стать узким местом при локальном поиске.
Решение:
score: function(search) {
return function(item) {
return item.title.includes(search) ? 1 : 0;
};
}
Позволяет полностью управлять моментом запуска запросов:
shouldLoad: function(query) {
return query.length >= 2;
}
Эффект:
Для огромных справочников (100k+ записей) используется incremental loading:
load: function(query, callback) {
fetch(`/api?q=${query}&page=1`)
.then(r => r.json())
.then(data => callback(data));
}
Дальнейшее расширение:
pagecursorhasMoreВ сочетании с кастомным UI можно реализовать “infinite dropdown”.
При открытии без поиска важно избегать полной загрузки.
openOnFocus: false,
preload: false
Или ленивый старт:
shouldLoad: query => query.length > 0
При длительной работе приложения кэш и DOM могут разрастаться.
Практики:
if (cache.size > 100) {
cache.delete(cache.keys().next().value);
}
onDropdownClose: function() {
cache.clear();
}
Каждое обновление списка вызывает перерасчёт DOM.
Оптимизация:
callback без измененийnew TomSelect("#select", {
valueField: "id",
labelField: "title",
searchField: ["title"],
loadThrottle: 300,
shouldLoad: query => query.length > 1,
maxOptions: 30,
load: function(query, callback) {
if (!query) return callback();
fetch(`/api/search?q=${query}&limit=30`)
.then(r => r.json())
.then(data => callback(data))
.catch(() => callback());
},
render: {
option: (data, escape) => `<div>${escape(data.title)}</div>`,
item: (data, escape) => `<div>${escape(data.title)}</div>`
}
});
При работе с действительно большими каталогами основной принцип остаётся неизменным: браузер никогда не должен владеть полным набором данных.
Ключевые архитектурные решения:
Такой подход позволяет интерфейсу сохранять стабильную отзывчивость даже при экстремальных объёмах данных, где клиентская обработка становится неприемлемой по производительности.