Code splitting

Code splitting — техника разделения JavaScript-приложения на несколько независимых частей (chunks), которые загружаются по мере необходимости. При использовании Kepler.gl данный подход особенно важен, поскольку библиотека включает значительный объём кода для визуализации геоданных, обработки слоёв, работы с картографическими движками и пользовательским интерфейсом.

В крупных приложениях загрузка всей функциональности Kepler.gl вместе с основным бандлом приводит к следующим проблемам:

  • увеличению времени первоначальной загрузки;
  • росту размера JavaScript-бандла;
  • повышенному потреблению памяти;
  • ухудшению показателей производительности в браузере;
  • замедлению Time To Interactive (TTI).

Code splitting позволяет загружать Kepler.gl только тогда, когда пользователь действительно открывает карту или работает с геопространственными данными.


Почему Kepler.gl часто требует ленивой загрузки

Архитектура Kepler.gl включает множество зависимостей:

  • React;
  • Redux;
  • deck.gl;
  • react-map-gl;
  • MapLibre или Mapbox;
  • наборы геопространственных утилит;
  • компоненты визуализации и анализа данных.

Даже минимальная интеграция карты может добавить в бандл несколько мегабайт JavaScript-кода.

Типичный сценарий:

  • главная страница содержит аналитику;
  • карта открывается в отдельном разделе;
  • большинство пользователей вообще не переходит к карте.

Без code splitting все пользователи будут загружать код Kepler.gl независимо от того, нужен он им или нет.


Принцип работы динамического импорта

Современный JavaScript предоставляет оператор import().

Обычный импорт:

import KeplerGl from 'kepler.gl';

Динамический импорт:

const KeplerGl = await import('kepler.gl');

В первом случае код включается в основной бандл.

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


Использование React.lazy

Наиболее распространённый способ подключения Kepler.gl в React-приложениях основан на React.lazy.

Создание отдельного компонента карты

// MapPage.jsx

import KeplerGl from 'kepler.gl';

export default function MapPage() {
  return (
    <KeplerGl
      id="map"
      width={window.innerWidth}
      height={window.innerHeight}
    />
  );
}

Ленивый импорт страницы

import React, { Suspense, lazy } from 'react';

const MapPage = lazy(() => import('./MapPage'));

function App() {
  return (
    <Suspense fallback={<div>Загрузка карты...</div>}>
      <MapPage />
    </Suspense>
  );
}

После сборки компонент карты будет находиться в отдельном JavaScript-файле.


Разделение по маршрутам

Один из наиболее эффективных подходов — загружать Kepler.gl только при переходе на соответствующий маршрут.

Пример для React Router:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const MapPage = lazy(() => import('./MapPage'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Dashboard />} />
          <Route path="/map" element={<MapPage />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Пользователь, работающий только с дашбордом, не загружает код карты.


Выделение Kepler.gl в отдельный chunk

Webpack автоматически создаёт отдельные чанки для динамических импортов.

Однако иногда требуется явно задать имя чанка.

const MapPage = lazy(() =>
  import(
    /* webpackChunkName: "kepler-map" */
    './MapPage'
  )
);

После сборки может появиться файл:

kepler-map.js

Такой подход упрощает анализ бандла и отладку.


Code Splitting через Next.js

В приложениях Next.js рекомендуется использовать функцию dynamic.

Базовый пример

import dynamic from 'next/dynamic';

const MapPage = dynamic(
  () => import('../components/MapPage'),
  {
    loading: () => <p>Loading map...</p>
  }
);

export default function Home() {
  return <MapPage />;
}

Отключение SSR для Kepler.gl

Kepler.gl зависит от браузерных API:

  • window;
  • document;
  • WebGL.

Поэтому серверный рендеринг часто вызывает ошибки.

Для Next.js обычно используется:

const MapPage = dynamic(
  () => import('../components/MapPage'),
  {
    ssr: false
  }
);

Теперь компонент будет загружаться только на клиенте.


Разделение тяжёлых обработчиков данных

Не только интерфейс карты может быть вынесен в отдельный chunk.

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

Пример:

async function loadGeoData() {
  const parser = await import('./geoParser');

  return parser.parse();
}

В результате модуль обработки данных не попадает в основной бандл.


Отложенная загрузка наборов данных

Большие GeoJSON-файлы могут достигать десятков мегабайт.

Нежелательно загружать их сразу после открытия приложения.

Вместо этого данные можно получать по требованию.

async function loadDataset() {
  const response = await fetch('/data/cities.geojson');

  return response.json();
}

Загрузка начинается только после открытия карты.


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

Иногда необходимо сохранить преимущества code splitting и одновременно уменьшить задержку открытия карты.

Для этого применяется prefetch.

Webpack поддерживает специальную директиву:

const MapPage = lazy(() =>
  import(
    /* webpackPrefetch: true */
    './MapPage'
  )
);

Браузер загрузит chunk в фоновом режиме, когда сеть будет свободна.

Пользователь увидит карту быстрее.


Preload и Prefetch

Эти механизмы решают разные задачи.

Preload

Загрузка начинается практически сразу.

<link
  rel="preload"
  href="/kepler-map.js"
  as="script"
/>

Используется для критически важных ресурсов.

Prefetch

Загрузка выполняется позже.

<link
  rel="prefetch"
  href="/kepler-map.js"
/>

Подходит для вероятных будущих переходов.

Для Kepler.gl обычно предпочтителен именно prefetch.


Анализ размеров бандла

После внедрения code splitting важно проверить результат.

Для Webpack используется Bundle Analyzer.

Установка:

npm install webpack-bundle-analyzer

Подключение:

const BundleAnalyzerPlugin =
  require('webpack-bundle-analyzer')
    .BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

Отчёт покажет:

  • размер Kepler.gl;
  • размер deck.gl;
  • вклад react-map-gl;
  • распределение зависимостей по чанкам.

Разделение vendor-зависимостей

Крупные приложения часто выделяют внешние библиотеки в отдельный vendor chunk.

Пример настройки Webpack:

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all'
      }
    }
  }
}

Преимущества:

  • кэширование библиотек;
  • уменьшение повторных загрузок;
  • ускорение навигации между страницами.

Комбинирование Code Splitting и Redux

Kepler.gl использует Redux для хранения состояния карты.

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

Пример динамического подключения:

const reducers = await import('./reducers');

store.replaceReducer(
  reducers.default
);

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


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

Некоторые функции используются редко:

  • экспорт изображений;
  • импорт файлов;
  • геоаналитика;
  • генерация отчётов.

Они также могут подключаться динамически.

async function exportMap() {
  const exporter =
    await import('./mapExporter');

  exporter.createImage();
}

Пользователь загрузит код экспорта только при нажатии соответствующей кнопки.


Стратегия модульной архитектуры

Для проектов с Kepler.gl рекомендуется следующая структура:

src/
├── pages/
│   ├── Dashboard
│   ├── Reports
│   └── MapPage
│
├── map/
│   ├── components
│   ├── layers
│   ├── datasets
│   ├── reducers
│   └── services

Модуль map становится полностью автономным и может загружаться отдельным chunk.


Избежание дублирования зависимостей

Неправильная конфигурация сборщика способна привести к ситуации, когда:

  • основное приложение содержит React;
  • chunk карты содержит ещё одну копию React.

Это увеличивает размер загрузки и может вызывать ошибки.

Необходимо контролировать:

resolve: {
  dedupe: [
    'react',
    'react-dom'
  ]
}

или использовать корректную настройку общих зависимостей в Webpack, Vite либо Rollup.


Особенности Code Splitting в Vite

Vite использует Rollup для production-сборок.

Для выделения Kepler.gl в отдельный chunk применяется настройка:

export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          kepler: [
            'kepler.gl',
            '@deck.gl/core',
            '@deck.gl/layers'
          ]
        }
      }
    }
  }
};

В результате формируется отдельный файл:

kepler.[hash].js

Оптимизация времени открытия карты

Даже после внедрения code splitting карта может загружаться заметно дольше остальных страниц.

Для ускорения рекомендуется:

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

Типичные ошибки при использовании Code Splitting

Импорт внутри основного файла

Неправильно:

import KeplerGl from 'kepler.gl';

Даже если компонент позже оборачивается в lazy, библиотека уже окажется в основном бандле.


Загрузка данных раньше компонента

Неправильно:

import geoData from './data.json';

Файл попадёт в главный bundle.

Лучше:

const data =
  await fetch('/data.json')
    .then(r => r.json());

Отсутствие fallback-интерфейса

Без Suspense пользователь может увидеть пустой экран.

Правильно:

<Suspense
  fallback={<Spinner />}
>
  <MapPage />
</Suspense>

Слишком мелкие чанки

Чрезмерное дробление приложения приводит к:

  • большому количеству HTTP-запросов;
  • росту сетевых накладных расходов;
  • ухудшению производительности.

Code splitting должен разделять логические модули, а не каждый файл отдельно.


Практическая схема для проектов с Kepler.gl

Эффективная организация загрузки обычно выглядит следующим образом:

  1. Загружается базовое приложение.
  2. Пользователь работает с интерфейсом.
  3. В фоне выполняется prefetch страницы карты.
  4. При переходе на карту загружается chunk Kepler.gl.
  5. После открытия карты запрашиваются данные.
  6. Редко используемые инструменты импортируются динамически.
  7. Экспорт, аналитика и отчёты подключаются только по запросу.

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