При работе с геопространственными приложениями объём передаваемых данных и размер клиентского кода оказывают непосредственное влияние на производительность. Карты часто содержат десятки слоёв, тысячи объектов и большое количество вспомогательных зависимостей. Если весь код и все данные загружаются одновременно при открытии страницы, возрастает время первоначального рендеринга, увеличивается потребление памяти и ухудшается пользовательский опыт.
Lazy loading (ленивая загрузка) — подход, при котором код, данные или ресурсы загружаются только в момент фактической необходимости.
В контексте Kepler.gl ленивую загрузку можно применять к нескольким категориям ресурсов:
Основная цель заключается в уменьшении размера первоначального JavaScript-бандла и сокращении времени загрузки приложения.
Картографические приложения отличаются от обычных веб-интерфейсов рядом особенностей:
При загрузке всех компонентов сразу возникают следующие проблемы:
Пользователь ожидает загрузки:
Даже при хорошем соединении это может привести к заметным задержкам.
Kepler.gl является достаточно крупной библиотекой. Если карта используется только на одной странице приложения, нецелесообразно включать её код в основной пакет, который загружается на всех страницах.
Многие данные могут никогда не понадобиться в рамках конкретной пользовательской сессии.
Наиболее распространённый сценарий — динамический импорт компонента карты.
import KeplerGl from 'kepler.gl';
function App() {
return (
<KeplerGl
id="map"
width={window.innerWidth}
height={window.innerHeight}
/>
);
}
В этом случае код Kepler.gl попадает в основной бандл приложения.
import React, { lazy, Suspense } from 'react';
const KeplerGl = lazy(() => import('kepler.gl'));
function App() {
return (
<Suspense fallback={<div>Загрузка карты...</div>}>
<KeplerGl
id="map"
width={window.innerWidth}
height={window.innerHeight}
/>
</Suspense>
);
}
Преимущества:
Поскольку Kepler.gl обычно используется вместе с React, механизм
React.lazy() является базовым инструментом реализации
ленивой загрузки.
const MapPage = React.lazy(() =>
import('./pages/MapPage')
);
При переходе на страницу:
<Route
path="/map"
element={
<Suspense fallback={<Spinner />}>
<MapPage />
</Suspense>
}
/>
происходит:
Одним из наиболее эффективных способов lazy loading является route-based code splitting.
src/
├── pages/
│ ├── HomePage.jsx
│ ├── ReportsPage.jsx
│ └── MapPage.jsx
├── App.jsx
└── store.js
const HomePage = lazy(() => import('./pages/HomePage'));
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
const MapPage = lazy(() => import('./pages/MapPage'));
Если карта используется редко, пользователи, которые никогда не открывают раздел картографии, не будут загружать Kepler.gl вовсе.
Загрузка самого компонента карты решает лишь часть проблемы. Часто значительно больший объём занимают данные.
import cityData from './data/cities.geojson';
При таком импорте данные включаются в бандл.
Для файлов размером десятки мегабайт это крайне неэффективно.
async function loadData() {
const response = await fetch('/data/cities.geojson');
return response.json();
}
Загрузка начинается только после необходимости отображения слоя.
Нередко карта должна появиться максимально быстро, а данные могут поступать позже.
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/locations');
const data = await response.json();
dispatch(addDataToMap(data));
}
fetchData();
}, []);
Преимущества:
GeoJSON способен содержать огромные массивы координат.
Пример:
{
"type": "FeatureCollection",
"features": [...]
}
Размер файла может достигать сотен мегабайт.
Полная загрузка такого ресурса приводит к:
Вместо одного большого файла используются пространственные фрагменты.
tiles/
├── 0_0.geojson
├── 0_1.geojson
├── 1_0.geojson
└── 1_1.geojson
Загружается только видимая область карты.
Для разных уровней масштабирования могут использоваться разные наборы данных.
При масштабе:
zoom < 6
используются агрегированные данные.
При масштабе:
zoom >= 6
загружаются детализированные данные.
Пример:
if (zoom < 6) {
loadRegions();
} else {
loadBuildings();
}
Подобный подход существенно уменьшает объём передаваемой информации.
Текущая область карты может использоваться для запроса только необходимых объектов.
const bounds = map.getBounds();
После этого выполняется запрос:
fetch(
`/api/objects?bbox=${bounds.toArray()}`
);
Сервер возвращает исключительно объекты внутри видимой области.
Это один из наиболее эффективных методов работы с большими пространственными данными.
Конфигурации карт также могут занимать значительный объём.
Например:
import worldConfig from './configs/world.json';
import europeConfig from './configs/europe.json';
import asiaConfig from './configs/asia.json';
Более эффективный вариант:
const config = await import(
`./configs/${region}.json`
);
Загружается только необходимая конфигурация.
В некоторых проектах создаются собственные расширения поверх Deck.gl и Kepler.gl.
const HeatmapLayer = lazy(() =>
import('./layers/HeatmapLayer')
);
Загрузка происходит только при выборе соответствующего режима визуализации.
Это особенно полезно для:
Kepler.gl активно использует Redux.
Большие наборы данных не следует помещать в store заранее.
Нежелательный вариант:
const initialState = {
hugeDataset
};
Лучше загружать данные после инициализации приложения.
dispatch(loadDataset());
Внутри thunk:
export const loadDataset = () => async dispatch => {
const response = await fetch('/api/data');
dispatch({
type: 'DATA_LOADED',
payload: await response.json()
});
};
Lazy loading часто комбинируется с механизмом предварительной загрузки.
Файл ещё не нужен сейчас, но с высокой вероятностью понадобится скоро.
Пример:
import(
/* webpackPrefetch: true */
'./MapPage'
);
Webpack создаёт специальную инструкцию браузеру.
Пока пользователь читает текущую страницу, карта может быть загружена в фоне.
В результате:
Интересный вариант оптимизации — загрузка карты при наведении курсора.
function preloadMap() {
import('./MapPage');
}
<button
onMouseEn ter={preloadMap}
>
Карта
</button>
Пока пользователь принимает решение о переходе, необходимые ресурсы уже начинают загружаться.
Ленивая загрузка требует правильного отображения промежуточных состояний.
<Suspense
fallback={<LoadingSpinner />}
>
<MapPage />
</Suspense>
<Suspense
fallback={<MapSkeleton />}
>
<MapPage />
</Suspense>
Скелетоны позволяют избежать ощущения «пустого экрана».
Сетевые ошибки должны учитываться заранее.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Ошибка загрузки карты</div>;
}
return this.props.children;
}
}
Использование:
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<MapPage />
</Suspense>
</ErrorBoundary>
Наиболее заметные улучшения обычно наблюдаются по следующим метрикам:
Первые элементы интерфейса отображаются быстрее.
Ускоряется появление основного контента страницы.
Приложение раньше становится интерактивным.
Уменьшается объём загружаемого JavaScript.
Разделение страниц:
Home
Analytics
Dashboard
Maps
Settings
Каждая страница становится отдельным chunk.
Разделение:
Map Core
Layers
Filters
Analytics
Export
Дополнительные модули загружаются по мере необходимости.
Разделение:
Countries
Regions
Cities
Buildings
POI
Каждый набор данных получает собственный жизненный цикл загрузки.
Вместо хранения всех данных на клиенте рекомендуется использовать серверную фильтрацию.
Пример:
fetch(
`/api/events?page=1&limit=500`
);
Затем:
fetch(
`/api/events?page=2&limit=500`
);
Такой подход сочетает:
Современные браузеры позволяют получать данные частями.
Пример использования потоков:
const response = await fetch(url);
const reader = response.body.getReader();
По мере поступления данных можно постепенно формировать наборы для отображения в Kepler.gl.
Это особенно актуально для:
Не включать GeoJSON напрямую в бандл приложения.
Даже относительно небольшие файлы желательно загружать отдельно.
Разделять маршруты приложения.
Страница карты должна представлять отдельный chunk.
Использовать динамические импорты для тяжёлых модулей.
Особенно для аналитики, 3D-слоёв и специализированных визуализаций.
Загружать данные по области просмотра карты.
Передача только видимых объектов значительно снижает сетевую нагрузку.
Комбинировать lazy loading и prefetch.
Так достигается баланс между скоростью открытия приложения и скоростью перехода к карте.
Использовать многоуровневую загрузку данных.
Сначала агрегированные данные, затем детализированные по мере увеличения масштаба.
Контролировать размеры chunk-файлов.
Даже после внедрения lazy loading отдельные модули не должны становиться чрезмерно большими.
Организовывать кэширование данных.
Повторная загрузка ранее полученных геоданных должна выполняться только при необходимости.
Обрабатывать ошибки и отображать промежуточные состояния.
Ленивая загрузка неизбежно увеличивает количество асинхронных операций, поэтому требуется надёжная система индикации загрузки и восстановления после сбоев.