Асинхронная загрузка данных

При работе с Awesomplete ключевым ограничением становится необходимость заранее передавать список данных. Для небольших наборов это не вызывает проблем, но при тысячах или миллионах записей клиентская загрузка становится неэффективной. Решение заключается в переходе к асинхронной модели, где данные запрашиваются динамически при вводе текста.

Базовый подход строится вокруг события ввода и вызова серверного API:

const input = document.querySelector("#city");

const awesomplete = new Awesomplete(input, {
    minChars: 2,
    maxItems: 10
});

input.addEventListener("input", function () {
    const query = this.value;

    if (query.length < 2) return;

    fetch(`/api/cities?q=${encodeURIComponent(query)}`)
        .then(res => res.json())
        .then(data => {
            awesomplete.list = data;
        });
});

Здесь список обновляется после каждого изменения поля ввода. Однако такой подход создаёт избыточное количество запросов, поэтому требуется контроль частоты обращений.

Ограничение частоты запросов через debounce

Асинхронная подгрузка без ограничения приводит к перегрузке сети и сервера. Для стабилизации используется техника debounce, при которой запрос отправляется только после паузы в наборе текста.

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

Применение:

const loadCities = debounce(function (query) {
    fetch(`/api/cities?q=${encodeURIComponent(query)}`)
        .then(res => res.json())
        .then(data => {
            awesomplete.list = data;
        });
}, 300);

input.addEventListener("input", function () {
    if (this.value.length >= 2) {
        loadCities(this.value);
    }
});

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

Использование динамического списка через функцию

Awesomplete поддерживает возможность передачи функции вместо статического массива. Это позволяет полностью контролировать процесс формирования подсказок.

new Awesomplete(input, {
    minChars: 2,
    list: function (text, callback) {
        fetch(`/api/cities?q=${encodeURIComponent(text)}`)
            .then(res => res.json())
            .then(data => callback(data));
    }
});

В этом случае библиотека сама инициирует запрос к списку через callback, а разработчик отвечает только за получение данных.

Такой подход имеет важное преимущество: исключается необходимость вручную присваивать awesomplete.list, а логика интеграции становится более декларативной.

Синхронизация состояния и устаревшие ответы

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

Решение заключается в введении идентификатора запроса:

let requestId = 0;

new Awesomplete(input, {
    minChars: 2,
    list: function (text, callback) {
        const currentId = ++requestId;

        fetch(`/api/cities?q=${encodeURIComponent(text)}`)
            .then(res => res.json())
            .then(data => {
                if (currentId !== requestId) return;
                callback(data);
            });
    }
});

Каждый новый ввод увеличивает requestId, и только последний ответ считается актуальным. Это предотвращает гонки данных и визуальные артефакты.

Предзагрузка и кэширование результатов

Для повышения производительности часто применяется кэширование результатов запросов. Это особенно эффективно при повторяющихся поисковых запросах.

const cache = new Map();

function fetchCities(query) {
    if (cache.has(query)) {
        return Promise.resolve(cache.get(query));
    }

    return fetch(`/api/cities?q=${encodeURIComponent(query)}`)
        .then(res => res.json())
        .then(data => {
            cache.set(query, data);
            return data;
        });
}

Интеграция с Awesomplete:

new Awesomplete(input, {
    minChars: 2,
    list: function (text, callback) {
        fetchCities(text).then(callback);
    }
});

Кэширование снижает нагрузку на сервер и уменьшает задержки при повторных запросах.

Интеграция с REST API и пагинацией

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

function fetchCities(query, page = 1) {
    return fetch(`/api/cities?q=${encodeURIComponent(query)}&page=${page}`)
        .then(res => res.json());
}

Расширенная логика:

new Awesomplete(input, {
    minChars: 2,
    list: function (text, callback) {
        fetchCities(text, 1).then(response => {
            callback(response.items);
        });
    }
});

При необходимости можно добавлять кнопку «показать ещё», но это уже выходит за рамки стандартного поведения автодополнения и требует кастомного UI.

Обработка ошибок при асинхронной загрузке

При нестабильной сети или ошибках API необходимо предусмотреть безопасное поведение, чтобы интерфейс не ломался.

new Awesomplete(input, {
    minChars: 2,
    list: function (text, callback) {
        fetch(`/api/cities?q=${encodeURIComponent(text)}`)
            .then(res => {
                if (!res.ok) throw new Error("Network error");
                return res.json();
            })
            .then(data => callback(data))
            .catch(() => callback([]));
    }
});

Возврат пустого массива гарантирует, что интерфейс не будет отображать устаревшие или повреждённые данные.

Комбинирование локальных и удалённых данных

Гибридная модель часто оказывается наиболее эффективной: часть данных хранится локально, а недостающие элементы подтягиваются с сервера.

const localCities = ["Almaty", "Astana", "Shymkent"];

new Awesomplete(input, {
    minChars: 1,
    list: function (text, callback) {
        const filteredLocal = localCities.filter(item =>
            item.toLowerCase().includes(text.toLowerCase())
        );

        if (filteredLocal.length >= 5) {
            callback(filteredLocal);
            return;
        }

        fetch(`/api/cities?q=${encodeURIComponent(text)}`)
            .then(res => res.json())
            .then(remote => {
                callback([...filteredLocal, ...remote]);
            });
    }
});

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

Оптимизация частичных запросов

При работе с серверной подсказкой важно минимизировать объём передаваемых данных. Один из методов — отправка только последних символов или использование префиксного поиска.

function optimizeQuery(text) {
    return text.trim().toLowerCase();
}

Применение:

list: function (text, callback) {
    const q = optimizeQuery(text);

    fetch(`/api/cities?q=${encodeURIComponent(q)}`)
        .then(res => res.json())
        .then(callback);
}

Такая нормализация позволяет серверу использовать индексы и ускоряет поиск.

Управление задержками отображения результатов

Даже при быстрой сети резкие обновления списка могут создавать визуальный шум. Добавление минимальной задержки перед отображением улучшает UX.

function delayedCallback(callback, delay = 100) {
    return function (data) {
        setTimeout(() => callback(data), delay);
    };
}

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

list: function (text, callback) {
    fetch(`/api/cities?q=${encodeURIComponent(text)}`)
        .then(res => res.json())
        .then(delayedCallback(callback));
}

Это сглаживает поведение интерфейса и делает обновление списка более естественным.

Поддержка отмены запросов через AbortController

Современный подход к асинхронной загрузке предполагает возможность отмены устаревших запросов.

let controller;

new Awesomplete(input, {
    minChars: 2,
    list: function (text, callback) {
        if (controller) controller.abort();

        controller = new AbortController();

        fetch(`/api/cities?q=${encodeURIComponent(text)}`, {
            signal: controller.signal
        })
            .then(res => res.json())
            .then(callback)
            .catch(err => {
                if (err.name !== "AbortError") {
                    callback([]);
                }
            });
    }
});

Этот механизм предотвращает обработку ненужных ответов и снижает нагрузку на клиент.

Согласованность данных и предсказуемость интерфейса

Асинхронная загрузка в Awesomplete требует строгого контроля состояния: актуальность запроса, кэширование, обработка ошибок и управление частотой вызовов. Без этих механизмов автодополнение превращается в источник нестабильного поведения интерфейса, особенно при высокой скорости ввода.

Корректно построенная система асинхронных подсказок обеспечивает предсказуемость, минимальную задержку и масштабируемость при работе с большими объёмами данных.