Утечка памяти (memory leak) возникает в ситуации, когда объекты продолжают оставаться доступными для сборщика мусора JavaScript даже после того, как необходимость в них исчезла. Со временем такие объекты накапливаются, увеличивая потребление оперативной памяти, замедляя работу интерфейса и приводя к деградации производительности.
В проектах на базе Kepler.gl проблема особенно актуальна по нескольким причинам:
При длительной работе приложения даже небольшие утечки могут привести к потреблению сотен мегабайт дополнительной памяти.
Архитектурно Kepler.gl строится вокруг нескольких крупных подсистем:
Каждая из них имеет собственные механизмы выделения ресурсов.
Основные категории объектов, занимающих память:
Удаление React-компонента само по себе не гарантирует освобождение всех связанных ресурсов. Если остаются ссылки на данные, сборщик мусора не сможет освободить память.
Одним из наиболее распространённых источников утечек является глобальное хранилище состояния.
Проблемный пример:
dispatch(addDataToMap({
datasets: {
info: {
id: 'dataset'
},
data: largeDataset
}
}));
Если пользователь многократно загружает новые наборы данных, старые объекты могут оставаться в состоянии приложения.
Например:
state.datasets.push(newDataset);
При отсутствии механизма очистки память будет постоянно расти.
Правильнее использовать замену данных:
state.dataset = newDataset;
или удаление старых наборов:
dispatch(removeDataset(datasetId));
Некоторые проекты реализуют собственные механизмы Undo/Redo.
Опасный вариант:
history.push(currentState);
Если состояние содержит крупные GeoJSON-объекты, каждый снимок состояния может занимать десятки мегабайт.
Лучше сохранять:
Например:
history.push({
filterId,
previousValue,
nextValue
});
Частая ошибка:
const geojsonCopy = JSON.parse(
JSON.stringify(geojson)
);
Каждое такое копирование удваивает объём занимаемой памяти.
Особенно опасны операции внутри циклов:
datasets.forEach(dataset => {
cache.push(
JSON.parse(JSON.stringify(dataset))
);
});
Для крупных файлов объём памяти может вырасти в несколько раз.
Предпочтительно использовать:
const datasetReference = geojson;
либо создавать производные структуры только при необходимости.
Пример:
let filteredResults = [];
function updateFilters(data) {
filteredResults.push(
data.features.filter(...)
);
}
Каждый вызов сохраняет новый массив.
Корректный подход:
function updateFilters(data) {
filteredResults =
data.features.filter(...);
}
Компонент может создавать подписку на внешние события:
useEffect(() => {
map.on('move', handleMove);
}, []);
После размонтирования компонента обработчик остаётся зарегистрированным.
Правильная реализация:
useEffect(() => {
map.on('move', handleMove);
return () => {
map.off('move', handleMove);
};
}, []);
Проблемный код:
useEffect(() => {
setInterval(() => {
updateStatistics();
}, 1000);
}, []);
После удаления компонента интервал продолжает работать.
Необходимо очищать таймер:
useEffect(() => {
const timer = setInterval(() => {
updateStatistics();
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
Пример:
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(setData);
}, []);
Если компонент удалён до завершения запроса, результаты могут удерживать память.
Более безопасный вариант:
useEffect(() => {
const controller =
new AbortController();
fetch(url, {
signal: controller.signal
});
return () => {
controller.abort();
};
}, []);
Kepler.gl активно использует deck.gl для визуализации слоёв.
Каждый слой создаёт:
Если слои создаются динамически, но не уничтожаются корректно, память видеокарты начинает расти.
Проблемный код:
const layers = [
new ScatterplotLayer({
id: Date.now().toString(),
data
})
];
Каждый рендер создаёт новый слой.
Лучше использовать стабильные идентификаторы:
const layers = [
new ScatterplotLayer({
id: 'points-layer',
data
})
];
При ручной работе с deck.gl необходимо вызывать очистку:
deck.finalize();
Метод освобождает:
Если экземпляр deck создаётся вручную:
const deck = new Deck({...});
то при завершении работы необходимо:
deck.finalize();
Сборщик мусора JavaScript не управляет памятью GPU напрямую.
Это означает, что объект может исчезнуть из памяти JavaScript, но соответствующие ресурсы видеокарты останутся занятыми.
Типичные проблемы:
Опасный сценарий:
function recreateMap() {
return new KeplerGl(...);
}
Если старый экземпляр не уничтожается, память GPU продолжает расти.
Перед созданием нового экземпляра необходимо удалить предыдущий.
Карта генерирует множество событий:
move
zoom
drag
resize
click
mousemove
Неправильный код:
map.on('move', updateLayer);
При повторном монтировании компонента количество обработчиков увеличивается:
map.on('move', updateLayer);
map.on('move', updateLayer);
map.on('move', updateLayer);
Каждый обработчик удерживает ссылки на:
В результате память постепенно растёт.
Необходимо удалять обработчики:
map.off('move', updateLayer);
Кэширование ускоряет работу приложения, но может стать причиной серьёзных утечек.
Проблемный пример:
const cache = new Map();
Далее:
cache.set(datasetId, dataset);
Если элементы никогда не удаляются:
cache.size
будет постоянно увеличиваться.
Необходимо реализовывать политику очистки.
Например:
if (cache.size > 100) {
cache.clear();
}
или механизм LRU-кэша.
JavaScript-замыкания способны удерживать крупные объекты даже после завершения работы функции.
Пример:
function createHandler(dataset) {
return () => {
console.log(dataset.id);
};
}
Если обработчик сохраняется:
handlers.push(
createHandler(largeDataset)
);
то весь набор данных остаётся в памяти.
Более безопасный вариант:
function createHandler(datasetId) {
return () => {
console.log(datasetId);
};
}
При создании собственных расширений для Kepler.gl важно контролировать жизненный цикл объектов.
Проблемный пример:
class CustomLayer {
constructor(data) {
this.cache = data;
}
}
Если экземпляры создаются многократно:
layers.push(
new CustomLayer(bigDataset)
);
старые данные остаются в памяти.
Следует реализовывать методы очистки:
destroy() {
this.cache = null;
}
и вызывать их перед удалением слоя.
Для временного хранения связанных объектов полезно использовать слабые коллекции.
Обычный вариант:
const metadata = new Map();
Без удаления элементов память будет накапливаться.
Вариант со слабой ссылкой:
const metadata =
new WeakMap();
После удаления объекта ключа данные автоматически становятся доступными для сборщика мусора.
Пример:
metadata.set(dataset, {
color: 'red'
});
После исчезновения объекта dataset запись также может
быть удалена движком JavaScript.
Для поиска утечек используются инструменты браузера.
Позволяет:
Типичный алгоритм:
Если количество объектов продолжает расти после очистки, существует утечка.
Инструмент показывает:
Постоянный рост JS Heap без возврата к исходному уровню является характерным признаком утечки памяти.
Полезен для выявления:
Особое внимание уделяется компонентам, связанным с:
При работе с данными:
При работе с React:
При работе с deck.gl и WebGL:
При работе с обработчиками событий:
off() после on();При профилировании:
Систематическое соблюдение этих правил позволяет поддерживать стабильное потребление памяти даже при обработке крупных геопространственных наборов данных и длительной работе приложений на базе Kepler.gl.