Ленивая загрузка в контексте компонентов выбора данных строится вокруг принципа отложенного получения и подстановки опций только в момент реальной необходимости. В библиотеке Slim Select этот подход используется для снижения начального веса данных, ускорения инициализации и уменьшения нагрузки на DOM при работе с большими списками.
Ключевая идея заключается в том, что список опций не передаётся полностью при инициализации, а подгружается частями — по мере взаимодействия пользователя с полем выбора.
Обычная HTML-реализация <select> становится
неэффективной при количестве опций от нескольких тысяч элементов:
Slim Select частично решает эту проблему виртуализацией и поисковой фильтрацией, однако без ленивой загрузки все данные всё равно должны быть загружены заранее.
Ленивая загрузка строится вокруг событийного запроса данных:
Таким образом, список формируется динамически.
При ленивой загрузке начальный список часто пустой:
new SlimSelect({
select: '#city-select',
data: []
});
Или используется минимальный набор placeholder-значений:
new SlimSelect({
select: '#city-select',
placeholder: 'Начните ввод для поиска'
});
Slim Select не навязывает конкретный API слой, поэтому ленивую загрузку реализуют через внешние запросы. Основной подход — обработка события поиска.
const select = new SlimSelect({
select: '#city-select',
data: []
});
const input = document.querySelector('#city-select');
let debounceTimer;
input.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const query = e.target.value;
const response = await fetch(`/api/cities?q=${query}`);
const data = await response.json();
select.setData(
data.map(item => ({
text: item.name,
value: item.id
}))
);
}, 300);
});
Без ограничения частоты запросов ленивый поиск превращается в нагрузку на сервер. Используется техника debounce:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Применение:
const loadCities = debounce(async (query) => {
const res = await fetch(`/api/cities?q=${query}`);
const data = await res.json();
select.setData(data.map(x => ({
text: x.name,
value: x.id
})));
}, 300);
Другой вариант ленивой загрузки — запрос при открытии dropdown:
new SlimSelect({
select: '#city-select',
onOpen: async () => {
const res = await fetch('/api/cities?limit=50');
const data = await res.json();
select.setData(
data.map(item => ({
text: item.name,
value: item.id
}))
);
}
});
Этот подход эффективен, если данные не зависят от поискового запроса.
При больших наборах данных применяется постраничная подгрузка:
let page = 1;
let loading = false;
async function loadNext(query) {
if (loading) return;
loading = true;
const res = await fetch(`/api/cities?q=${query}&page=${page}`);
const data = await res.json();
select.setData([
...select.data,
...data.map(item => ({
text: item.name,
value: item.id
}))
]);
page++;
loading = false;
}
Без кэширования повторные запросы с одинаковыми параметрами создают лишнюю нагрузку.
const cache = new Map();
async function fetchCities(query) {
if (cache.has(query)) {
return cache.get(query);
}
const res = await fetch(`/api/cities?q=${query}`);
const data = await res.json();
cache.set(query, data);
return data;
}
Кэш особенно важен при автодополнении, где пользователи часто вводят и стирают текст.
При быстром вводе предыдущие запросы становятся устаревшими. Используется AbortController:
let controller;
async function loadCities(query) {
if (controller) {
controller.abort();
}
controller = new AbortController();
const res = await fetch(`/api/cities?q=${query}`, {
signal: controller.signal
});
const data = await res.json();
select.setData(
data.map(item => ({
text: item.name,
value: item.id
}))
);
}
Это предотвращает гонки ответов (race conditions).
Ленивая загрузка наиболее эффективна при переносе фильтрации на сервер:
Пример API-логики:
GET /api/cities?q=al&page=1&limit=20
Ответ:
[
{ "id": 1, "name": "Almaty" },
{ "id": 2, "name": "Aldan" }
]
Slim Select поддерживает встроенный поиск, но при ленивой загрузке он часто отключается:
new SlimSelect({
select: '#city-select',
searchText: false
});
Вместо этого поиск реализуется внешне, а Slim Select используется только как рендеринг-слой.
Частое обновление data приводит к перерисовке DOM. Для оптимизации используется:
let lastQuery = '';
async function update(query) {
if (query === lastQuery) return;
lastQuery = query;
const data = await fetchCities(query);
select.setData(
data.map(x => ({
text: x.name,
value: x.id
}))
);
}
На практике используется комбинация стратегий:
async function init() {
const popular = await fetch('/api/cities?popular=1');
select.setData(
popular.map(x => ({
text: x.name,
value: x.id
}))
);
}
При задержках сети важно обеспечить предсказуемый UX:
async function load(query) {
select.setData([{ text: 'Загрузка...', value: '' }]);
const data = await fetchCities(query);
select.setData(
data.map(x => ({
text: x.name,
value: x.id
}))
);
}
При ленивой загрузке возникает проблема: выбранное значение может отсутствовать в текущем наборе данных. Решается через явную подстановку:
function ensureSelected(option) {
select.setData([
{
text: option.text,
value: option.value,
selected: true
}
]);
}
Несмотря на эффективность, подход имеет ограничения:
При неправильной реализации компонент может терять консистентность данных.
Стабильная архитектура ленивой загрузки обычно включает:
Такая схема делает компонент масштабируемым даже при миллионах записей в источнике данных.