Code splitting

В веб-приложениях с интерактивными картами на базе Leaflet объём JavaScript быстро растёт за счёт самой библиотеки, сторонних плагинов, слоёв данных, геокодинга, маршрутизации, кластеризации маркеров и пользовательской логики. Без архитектурного разделения всё это попадает в один бандл, увеличивая время первой загрузки, блокируя рендер и ухудшая показатели LCP и TTI.

Разделение кода позволяет загружать только ту часть функциональности, которая действительно необходима в текущем сценарии работы карты. Остальные модули подключаются по мере необходимости — при открытии конкретного режима, взаимодействии с интерфейсом или переходе в другую часть приложения.

Leaflet сам по себе относительно лёгкий, но экосистема вокруг него часто включает тяжёлые компоненты:

  • кластеризацию маркеров
  • тепловые карты
  • геокодеры
  • маршрутизацию
  • пользовательские контролы
  • обработку GeoJSON большого объёма

Идея разделения кода заключается в том, что базовая карта и минимальный слой отображения загружаются сразу, а расширенные возможности — только при необходимости.

Минимальная инициализация карты остаётся в основном бандле:

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);
  });
}

Интеграция с bundler-ами (Webpack, Vite, Rollup)

Разделение кода в Leaflet-приложениях обычно реализуется через сборщики.

Webpack

Webpack автоматически выделяет динамические импорты в отдельные чанки:

map.on("click", async () => {
  const { createPopupEditor } = await import("./popupEditor");
  createPopupEditor(map);
});

Каждый import() становится отдельным chunk-файлом, который загружается по требованию.

Vite

Vite использует нативный ES Modules и также автоматически разбивает код:

button.addEventListener("click", async () => {
  const module = await import("./geoTools");
  module.drawPolygon(map);
});

Важная особенность — более агрессивная оптимизация предварительной загрузки зависимостей.

Разделение плагинов Leaflet

Многие плагины Leaflet тяжёлые и редко нужны одновременно. Подключение всех сразу приводит к избыточной нагрузке.

Типичные кандидаты для ленивой загрузки:

  • leaflet.markercluster
  • leaflet-draw
  • leaflet-routing-machine
  • leaflet-heat

Пример загрузки инструмента рисования:

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 и больших наборов данных

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);
});

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

Разделение UI-контролов карты

Контролы 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

  • API запросы
  • кеширование
  • загрузка тайлов и GeoJSON

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

Ошибки при разделении кода в Leaflet-приложениях

Некоторые паттерны приводят к ухудшению производительности:

  • слишком мелкое дробление модулей, создающее избыточное количество запросов
  • загрузка Leaflet в каждом динамическом модуле вместо общего singleton
  • отсутствие кеширования импортов
  • дублирование плагинов в разных чанках
  • смешивание UI и data-логики в одном ленивом модуле

Особенно критично повторное подключение Leaflet:

// антипаттерн
const L = await import("leaflet"); // в каждом модуле

Оптимально использовать общий слой и передавать зависимости внутрь модулей.

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

Сборка должна стремиться к балансу между:

  • размером initial bundle
  • количеством async chunks
  • повторным использованием зависимостей

Часто оптимальная стратегия выглядит так:

  • core chunk: Leaflet + базовая карта
  • vendor chunk: общие библиотеки
  • feature chunks: маршруты, рисование, аналитика
  • data chunks: GeoJSON и API данные

Такой подход позволяет контролировать загрузку карты даже в сложных GIS-интерфейсах.