Ленивая загрузка данных (Lazy Loading) — подход, при котором данные
для элементов интерфейса загружаются не заранее, а только в момент
необходимости. В контексте Choices.js это особенно важно
при работе с:
Без ленивой загрузки браузер вынужден сразу обрабатывать огромный
массив <option>, что приводит к:
Choices.js позволяет реализовать загрузку данных по
требованию через:
fetch;async/await;Обычный <select> содержит все данные заранее:
<select id="cities">
<option value="1">Москва</option>
<option value="2">Санкт-Петербург</option>
<option value="3">Казань</option>
</select>
При ленивой загрузке список изначально пуст:
<select id="cities"></select>
Данные появляются только после:
<select id="users"></select>
const choices = new Choices('#users', {
searchEnabled: true,
shouldSort: false
});
На данном этапе список пустой.
async function loadUsers() {
const response = await fetch('/api/users');
const users = await response.json();
choices.setChoices(
users,
'id',
'name',
true
);
}
loadUsers();
Метод:
setChoices(array, valueKey, labelKey, replaceChoices)
| Аргумент | Назначение |
|---|---|
array |
Массив данных |
valueKey |
Поле value |
labelKey |
Поле текста |
replaceChoices |
Заменять ли старые элементы |
[
{
"id": 1,
"name": "Иван"
},
{
"id": 2,
"name": "Пётр"
}
]
Часто данные не нужны сразу после загрузки страницы. Рациональнее загружать их только при первом открытии селекта.
let loaded = false;
const choices = new Choices('#users');
document
.getElementById('users')
.addEventListener('showDropdown', async () => {
if (loaded) {
return;
}
const response = await fetch('/api/users');
const users = await response.json();
choices.setChoices(users, 'id', 'name', true);
loaded = true;
});
Без флага:
let loaded = false;
каждое открытие списка отправляло бы новый запрос.
Это может привести к:
Один из самых востребованных сценариев — подгрузка данных во время поиска.
const choices = new Choices('#users', {
searchEnabled: true,
searchChoices: false
});
Параметр:
searchChoices: false
отключает встроенный поиск по локальному массиву.
Теперь поиск будет выполняться сервером.
const input = document.querySelector('.choices__input');
input.addEventListener('input', async (event) => {
const query = event.target.value;
const response = await fetch(
`/api/users?q=${query}`
);
const users = await response.json();
choices.clearChoices();
choices.setChoices(
users,
'id',
'name',
true
);
});
При быстром вводе:
a
ab
abc
abcd
отправится 4 запроса подряд.
Это создаёт:
Debounce ограничивает частоту вызова функции.
function debounce(callback, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}
const input = document.querySelector('.choices__input');
input.addEventListener(
'input',
debounce(async (event) => {
const query = event.target.value;
if (query.length < 2) {
return;
}
const response = await fetch(
`/api/users?q=${query}`
);
const users = await response.json();
choices.clearChoices();
choices.setChoices(
users,
'id',
'name',
true
);
}, 400)
);
Практически всегда серверный поиск ограничивают минимальной длиной строки.
Например:
if (query.length < 2) {
return;
}
или:
if (query.trim().length < 3) {
return;
}
Это уменьшает количество запросов.
Во время загрузки пользователь должен видеть, что система работает.
choices.setChoices([
{
value: '',
label: 'Загрузка...',
disabled: true
}
], 'value', 'label', true);
async function searchUsers(query) {
choices.clearChoices();
choices.setChoices([
{
value: '',
label: 'Загрузка...',
disabled: true
}
], 'value', 'label', true);
const response = await fetch(
`/api/users?q=${query}`
);
const users = await response.json();
choices.clearChoices();
choices.setChoices(
users,
'id',
'name',
true
);
}
Сетевые запросы всегда могут завершиться ошибкой.
async function loadUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка сервера');
}
const users = await response.json();
choices.setChoices(
users,
'id',
'name',
true
);
} catch (error) {
choices.clearChoices();
choices.setChoices([
{
value: '',
label: 'Ошибка загрузки',
disabled: true
}
], 'value', 'label', true);
console.error(error);
}
}
Перед новой загрузкой обычно выполняют:
choices.clearChoices();
Иначе:
Ленивая загрузка особенно полезна в связанных списках.
Пример:
<select id="country"></select>
<select id="city"></select>
const countryChoices = new Choices('#country');
const cityChoices = new Choices('#city');
document
.getElementById('country')
.addEventListener('change', async (event) => {
const countryId = event.target.value;
cityChoices.clearChoices();
const response = await fetch(
`/api/cities?country=${countryId}`
);
const cities = await response.json();
cityChoices.setChoices(
cities,
'id',
'name',
true
);
});
После смены родительского элемента обычно сбрасывают дочерний список.
cityChoices.removeActiveItems();
Если данных очень много, используется постраничная загрузка.
Например:
let page = 1;
async function loadNextPage() {
const response = await fetch(
`/api/users?page=${page}`
);
const users = await response.json();
choices.setChoices(
users,
'id',
'name',
false
);
page++;
}
Ключевой момент:
false
вместо:
true
Теперь новые элементы добавляются, а не заменяют старые.
Во время infinite scroll легко отправить несколько одинаковых запросов подряд.
let loading = false;
async function loadNextPage() {
if (loading) {
return;
}
loading = true;
try {
const response = await fetch(
`/api/users?page=${page}`
);
const users = await response.json();
choices.setChoices(
users,
'id',
'name',
false
);
page++;
} finally {
loading = false;
}
}
При повторных запросах полезно использовать кэш.
const cache = {};
async function searchUsers(query) {
if (cache[query]) {
choices.clearChoices();
choices.setChoices(
cache[query],
'id',
'name',
true
);
return;
}
const response = await fetch(
`/api/users?q=${query}`
);
const users = await response.json();
cache[query] = users;
choices.clearChoices();
choices.setChoices(
users,
'id',
'name',
true
);
}
При быстром вводе старые запросы могут приходить позже новых.
Это создаёт проблему гонки запросов.
let controller;
async function searchUsers(query) {
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(
`/api/users?q=${query}`,
{
signal: controller.signal
}
);
const users = await response.json();
choices.clearChoices();
choices.setChoices(
users,
'id',
'name',
true
);
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
}
Choices.js поддерживает режим тегов.
const choices = new Choices('#tags', {
removeItemButton: true,
duplicateItemsAllowed: false
});
async function loadTags() {
const response = await fetch('/api/tags');
const tags = await response.json();
choices.setChoices(
tags,
'id',
'title',
true
);
}
Часто используют комбинированную стратегию:
await loadPopularUsers();
enableRemoteSearch();
Такой подход:
Массивы на десятки тысяч элементов создают проблемы даже при ленивой загрузке.
Плохо:
const users = 100000;
Хорошо:
fetch('/api/users?page=1');
Плохо:
searchChoices: true
для огромных массивов.
Лучше:
searchChoices: false
и серверный поиск.
Сервер не должен возвращать тысячи записей.
Оптимально:
20–50 элементов
Типичный API:
/api/users?page=1&limit=20
или:
/api/users?q=alex&page=2
{
"items": [
{
"id": 1,
"name": "Alex"
}
],
"hasMore": true
}
if (data.hasMore) {
loadNextButton.style.display = 'block';
}
При динамических интерфейсах важно корректно удалять экземпляры Choices.js.
choices.destroy();
input.removeEventListener('input', handler);
Плохо:
new Choices('#users');
new Choices('#users');
Плохо:
choices.setChoices(data);
choices.setChoices(data2);
Плохо:
input.addEventListener('input', async () => {
await fetch(...);
});
Без AbortController результаты могут приходить в
неправильном порядке.
Крупные проекты обычно разделяют:
class UsersService {
async search(query) {
const response = await fetch(
`/api/users?q=${query}`
);
return response.json();
}
}
class UsersSelect {
constructor(selector) {
this.choices = new Choices(selector, {
searchChoices: false
});
this.service = new UsersService();
}
async search(query) {
const users = await this.service.search(query);
this.choices.clearChoices();
this.choices.setChoices(
users,
'id',
'name',
true
);
}
}
Вместо пустого списка:
Загрузка...
используют placeholder-элементы.
Иногда новые данные подгружаются без очистки текущего списка, чтобы избежать визуальных скачков.
Дополнительно применяют:
На мобильных устройствах проблемы производительности проявляются сильнее:
Поэтому lazy loading становится обязательным подходом.
Особенно при:
Оптимально:
10–30 элементов
Наиболее распространённый диапазон:
300–500 мс
Лучшее решение:
AbortController
Особенно для:
Даже если API позволяет вернуть все данные одним запросом.