Много точек в Leaflet: ограничение отрисовки
Оглавление серии «Карты для веба» · Урок 10 из 12.
Четыре маркера не создают серьёзной нагрузки, но устройство небольшого примера можно подготовить к большему набору. В этом уроке отделим результат фильтрации от фактически нарисованных точек, ограничим число маркеров и добавим постепенное раскрытие списка. Это не измерение предельной производительности Leaflet: мы вводим понятные ограничения учебного клиента и разбираем, как они влияют на пользователя.
Каталог по-прежнему принимает не более 500 объектов. На карте рисуем до 400, а список сначала показывает до 50 записей. Дополнительный режим оставляет маркеры только в текущей области камеры. Ни одно из этих ограничений не удаляет места из принятого GeoJSON. Статус должен различать количество найденных объектов и число маркеров, чтобы сокращённое отображение не выглядело потерей данных.
Три набора вместо одного числа
Первый набор — все catalog.features. Второй — результат категории, текста и расстояния. Третий — точки, выбранные для отрисовки после ограничения областью и размером. Если смешать их, надпись «найдено 50» может означать и реальный поиск, и всего лишь размер первой порции. Для человека это разные ситуации: в одном случае других мест нет, в другом их можно открыть.
Список относится к результату поиска, поэтому при включённом режиме области он не теряет все записи за экраном. Маркеры относятся к пространственному представлению и могут быть ограничены камерой. Пользователь видит название за пределами карты и способен выбрать его: камера перейдёт к месту, а затем обновит соответствующие маркеры. Такое поведение нужно сохранять при любой оптимизации.
Предел 400 выбран для учебного договора, а не получен из профилирования. На слабом устройстве даже это число может быть слишком большим, особенно со сложными карточками. Для настоящего выпуска сначала измерьте длительность обновлений, число DOM-элементов и отзывчивость на целевых устройствах. Не называйте произвольную константу доказательством быстрой работы.
Ограничим карту видимой областью
Когда включён #viewport, проверяем положение точки через границы Leaflet:
const inView = features.filter(feature => {
const [lng, lat] = feature.geometry.coordinates;
return map.getBounds().contains([lat, lng]);
});
const shown = inView.slice(0, 400);
Это простая проверка для нашего городского каталога. Она проходит по всем найденным точкам, поэтому сама по себе не становится пространственным индексом. Для десятков тысяч записей потребуется иной источник выдачи, индекс на сервере или подготовленная структура в клиенте. Здесь важно понять границу ответственности: меньше рисовать не означает меньше скачать или меньше просмотреть в вычислениях.
Обновление привязано к moveend, а не к каждому кадру движения. Человек переместил камеру — приложение пересчитало видимый набор. Подложка при этом продолжает работать по своим правилам. Мы не используем невидимые перемещения, чтобы заранее скачать тайлы, и не связываем оптимизацию собственных маркеров с массовой загрузкой чужой карты.
Если выбранный объект входит в область, но оказался за пределом первых 400 записей, добавляем его в нарисованный набор, при необходимости убрав последнюю обычную точку. Иначе выбор из списка мог бы показать пустое место без карточки. Приоритет выбранного объекта — правило интерфейса, которое делает сокращение отображения объяснимым.
Обновим существующие маркеры
В функции renderMarkers используем режим incremental: true. Реестр сравнивает желаемые идентификаторы с уже существующими. Отсутствующие точки удаляются из слоя, новые создаются, существующие получают новые координаты и карточки. Центральная часть удаления выглядит так:
const desired = new Set(features.map(feature => feature.id));
for (const [id, marker] of markers) {
if (!desired.has(id)) {
markerLayer.removeLayer(marker);
markers.delete(id);
}
}
Идентификатор сохраняет связь объекта при изменении координат. Если сравнивать только массивы по ссылке, каждый новый результат фильтрации выглядел бы полностью новым набором. Если сравнивать только координаты, два места в одном здании могли бы слиться. Договор id из третьего урока теперь помогает оптимизации без изменения смысла данных.
В нашем модуле обновляется и popup существующего маркера. Это важно для последующего импорта: название может поменяться, а идентификатор остаться прежним. Для ещё более экономного обновления можно хранить принятую ревизию свойств и не пересоздавать карточку без изменений. Сейчас предпочитаем простой код, который гарантированно применяет новые данные, не выдавая его за окончательную оптимизацию.
Название также обновляется в title и alt уже созданной иконки и её параметрах. Одной замены popup недостаточно: видимая карточка могла бы получить новое имя, а подпись маркера для клавиатурного взаимодействия — остаться старой. Метод getElement даёт элемент существующей иконки; для стандартного изображения назначаем текстовые атрибуты без создания HTML.
Раскроем список порциями
Переменная listLimit начинается с 50. renderList строит только эту часть результата, а кнопка «Ещё 50 записей» увеличивает предел. Когда показаны все найденные места, кнопка отключается. Изменение категории или запроса возвращает предел к первой порции, потому что человек получил новый набор, а не продолжил старую выдачу.
Это ещё не виртуальный список. При раскрытии ста записей в DOM будут сто записей; невидимые строки не заменяются пустыми промежутками. Такой приём прост и подходит умеренному размеру. Виртуализация требует управления высотой, фокусом и чтением вспомогательными технологиями, поэтому её нельзя подменять одной функцией slice.
Клик по маркеру тоже может выбрать запись за пределами первой порции. Поэтому select находит её индекс в полном результате и при необходимости раскрывает список до этой строки. После обновления нужная кнопка существует и получает aria-pressed. Иначе карта могла бы выбирать одно из четырёхсот мест, а список из первых пятидесяти не показывал бы выбор вовсе. Автоматическое раскрытие связано с явным выбором места, а не с каждым движением камеры.
Сравните два вида пагинации. Здесь все данные уже находятся в памяти, а кнопка ограничивает только отображение. При серверной выдаче новая порция потребует запроса, состояния ожидания, обработки ошибки и устойчивого курсора. Надпись «Ещё 50» не означает одинаковое устройство этих вариантов; урок работает с локальным набором, который мы уже приняли целиком.
Выбор не должен исчезнуть из-за оптимизации
Когда человек выбирает место из списка, контроллер сначала переводит камеру к его координатам. Затем обновляет отображение с учётом новой области и открывает карточку. Если маркер отсутствовал, он появится после пересчёта. Ранее простой selectMarker мог найти только уже нарисованный объект; при ограничении областью этого недостаточно.
Перестроение списка создаёт новые кнопки, поэтому после такого перехода возвращаем фокус кнопке с тем же идентификатором и используем preventScroll. Это сохраняет возможность продолжить чтение названий. Для большого интерфейса предпочтительно обновлять сами строки по ключу и реже пересоздавать DOM, но учебный код явно показывает, где возникает проблема фокуса.
Если фильтр убрал выбранное место из результата, выбор сбрасывается по нашему прежнему правилу. Если место лишь оказалось за границей карты, его идентичность не меняется. Это различие особенно важно при серверных тайлах объектов или кластеризации: временно отсутствующий значок не равен удалённой записи в модели.
Как расширять пример дальше
Для плотного набора точек можно использовать кластеризацию, Canvas-представление или серверное разбиение. Каждый вариант меняет взаимодействие: кластер обозначает группу, кружок Canvas может иметь другую клавиатурную доступность, серверная выдача может не содержать все объекты. В этой серии не подключаем непроверенное дополнение только ради декоративных кластеров. Сначала определяем, какие действия и подписи должны сохраниться.
Учебные четыре места остаются удобными для проверки механизма. Добавьте в собственной копии больше синтетических записей с уникальными идентификаторами, затем включите область просмотра и переместите карту. Ожидается изменение числа маркеров при сохранении общего числа найденных мест. Выберите запись вне экрана: она должна стать видимой и открыть карточку. Измерения скорости на большом наборе при подготовке урока не проводились.
Полный снимок этого шага находится в папке lesson-10 архива серии. При подготовке материала пример не запускался; описанные эффекты ожидаются по разобранному коду и требуют самостоятельной проверки в браузере.