В веб-приложениях с интерактивными картами на базе Leaflet объём JavaScript быстро растёт за счёт самой библиотеки, сторонних плагинов, слоёв данных, геокодинга, маршрутизации, кластеризации маркеров и пользовательской логики. Без архитектурного разделения всё это попадает в один бандл, увеличивая время первой загрузки, блокируя рендер и ухудшая показатели LCP и TTI.
Разделение кода позволяет загружать только ту часть функциональности, которая действительно необходима в текущем сценарии работы карты. Остальные модули подключаются по мере необходимости — при открытии конкретного режима, взаимодействии с интерфейсом или переходе в другую часть приложения.
Leaflet сам по себе относительно лёгкий, но экосистема вокруг него часто включает тяжёлые компоненты:
Идея разделения кода заключается в том, что базовая карта и минимальный слой отображения загружаются сразу, а расширенные возможности — только при необходимости.
Минимальная инициализация карты остаётся в основном бандле:
import L from "leaflet";
const map = L.map("map").setView([51.505, -0.09], 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors"
}).addTo(map);
Все дополнительные функции выносятся в отдельные модули, которые подгружаются динамически.
Современный стандарт разделения кода в экосистеме JavaScript —
динамический import(). Он позволяет отложить загрузку
модуля до момента, когда он действительно нужен.
Пример подключения слоя кластеризации только при необходимости:
async function enableClustering(map, points) {
const { default: L } = await import("leaflet");
const { default: Supercluster } = await import("supercluster");
const index = new Supercluster({
radius: 60,
maxZoom: 16
});
index.load(points);
const markers = L.layerGroup();
points.forEach(point => {
const marker = L.marker([point.lat, point.lng]);
markers.addLayer(marker);
});
map.addLayer(markers);
}
Такой подход исключает загрузку supercluster и
дополнительной логики до момента активации слоя кластеризации.
Картографические приложения редко используют все функции одновременно. Типичная архитектура делит систему на сценарные модули:
Каждый сценарий оформляется как отдельный пакет или модуль.
function loadRouteModule(map) {
return import("./modules/route.js").then(module => {
module.initRouting(map);
});
}
function loadHeatmapModule(map) {
return import("./modules/heatmap.js").then(module => {
module.initHeatmap(map);
});
}
Разделение кода в Leaflet-приложениях обычно реализуется через сборщики.
Webpack автоматически выделяет динамические импорты в отдельные чанки:
map.on("click", async () => {
const { createPopupEditor } = await import("./popupEditor");
createPopupEditor(map);
});
Каждый import() становится отдельным chunk-файлом,
который загружается по требованию.
Vite использует нативный ES Modules и также автоматически разбивает код:
button.addEventListener("click", async () => {
const module = await import("./geoTools");
module.drawPolygon(map);
});
Важная особенность — более агрессивная оптимизация предварительной загрузки зависимостей.
Многие плагины Leaflet тяжёлые и редко нужны одновременно. Подключение всех сразу приводит к избыточной нагрузке.
Типичные кандидаты для ленивой загрузки:
Пример загрузки инструмента рисования:
async function enableDrawing(map) {
const L = await import("leaflet");
await import("leaflet-draw");
const drawControl = new L.Control.Draw({
edit: {
featureGroup: new L.FeatureGroup()
}
});
map.addControl(drawControl);
}
GeoJSON-слои часто становятся основной причиной тормозов. Разделение кода здесь сочетается с разделением данных.
Подход заключается в том, что:
map.on("moveend", async () => {
const bounds = map.getBounds();
const response = await fetch(`/api/geo?bbox=${bounds.toBBoxString()}`);
const data = await response.json();
const { default: L } = await import("leaflet");
L.geoJSON(data).addTo(map);
});
Такой подход уменьшает начальную нагрузку и позволяет масштабировать карты с десятками тысяч объектов.
Контролы Leaflet часто содержат отдельную бизнес-логику и могут быть вынесены в ленивые модули.
async function addSearchControl(map) {
const { SearchControl } = await import("./controls/searchControl");
const control = new SearchControl();
map.addControl(control);
}
Это особенно эффективно, если интерфейс содержит много специализированных инструментов, но пользователь использует только часть из них.
Разделение кода не ограничивается простым import().
Важную роль играет стратегия загрузки:
Можно использовать предзагрузку:
const heatmapPromise = import("./heatmap");
button.addEventListener("click", async () => {
const { initHeatmap } = await heatmapPromise;
initHeatmap(map);
});
Такой подход снижает задержку при первом использовании функции.
В крупных проектах с Leaflet код делится на слои:
1. Presentation layer
2. Domain layer
3. Data layer
Каждый слой может быть вынесен в отдельные чанки, особенно если используется микрофронтенд или модульная архитектура.
Некоторые паттерны приводят к ухудшению производительности:
Особенно критично повторное подключение Leaflet:
// антипаттерн
const L = await import("leaflet"); // в каждом модуле
Оптимально использовать общий слой и передавать зависимости внутрь модулей.
Сборка должна стремиться к балансу между:
Часто оптимальная стратегия выглядит так:
Такой подход позволяет контролировать загрузку карты даже в сложных GIS-интерфейсах.