Геопространственные вычисления часто связаны с обработкой больших наборов данных: тысяч точек, сотен полигонов, дорожных сетей, GPS-треков и спутниковых данных. Многие функции Turf.js выполняют сложные математические операции над координатами, что может создавать значительную нагрузку на процессор.
При обработке небольших коллекций объектов производительности JavaScript обычно достаточно. Однако при работе с крупными GeoJSON-файлами возникают следующие проблемы:
Параллельная обработка позволяет распределять вычисления между несколькими потоками исполнения и существенно сокращать время выполнения ресурсоёмких операций.
JavaScript традиционно работает в рамках одного основного потока выполнения.
Рассмотрим пример вычисления буферных зон вокруг десяти тысяч точек:
import * as turf from "@turf/turf";
const buffers = points.features.map(point =>
turf.buffer(point, 1, { units: "kilometers" })
);
Если коллекция содержит большое количество объектов, выполнение такого кода может занять несколько секунд. В браузере в этот момент:
Причина заключается в том, что все вычисления выполняются в основном потоке.
Для Turf.js используются несколько основных стратегий:
Большой набор данных разбивается на небольшие блоки.
Например:
function chunkArray(array, size) {
const result = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
Использование:
const chunks = chunkArray(points.features, 500);
Каждый блок затем может обрабатываться независимо.
В браузере наиболее распространённым механизмом параллельного выполнения являются Web Workers.
Они позволяют запускать вычисления в отдельных потоках без блокировки интерфейса.
Схема работы:
В серверной среде Node.js используются Worker Threads.
Они обеспечивают реальное многопоточное выполнение JavaScript-кода и особенно полезны для:
Файл:
// geoWorker.js
importScripts(
"https://unpkg.com/@turf/turf@latest/turf.min.js"
);
self.onmess age = function(event) {
const features = event.data;
const result = features.map(feature =>
turf.buffer(feature, 2, {
units: "kilometers"
})
);
self.postMessage(result);
};
Worker получает набор объектов и создаёт буфер вокруг каждого.
Основной поток:
const worker = new Worker("geoWorker.js");
worker.onmess age = function(event) {
const buffers = event.data;
console.log(buffers);
};
Передача данных:
worker.postMessage(points.features);
Интерфейс при этом остаётся полностью отзывчивым.
Одного Worker-потока бывает недостаточно.
Допустим, имеется 20 000 точек.
Данные можно разделить на четыре части:
const chunks = chunkArray(
points.features,
5000
);
Создание группы Worker-потоков:
const workers = chunks.map(() =>
new Worker("geoWorker.js")
);
Запуск вычислений:
const promises = workers.map((worker, index) => {
return new Promise(resolve => {
worker.onmess age = event => {
resolve(event.data);
};
worker.postMessage(chunks[index]);
});
});
Получение результатов:
const results = await Promise.all(promises);
const merged = results.flat();
Вычисления выполняются одновременно в нескольких потоках.
Часто требуется обработать весь GeoJSON-документ.
Исходные данные:
{
"type": "FeatureCollection",
"features": [...]
}
Разбиение:
const chunkSize = 1000;
const chunks = [];
for (
let i = 0;
i < collection.features.length;
i += chunkSize
) {
chunks.push(
collection.features.slice(
i,
i + chunkSize
)
);
}
Каждый блок отправляется отдельному Worker-потоку.
После завершения вычислений результаты объединяются:
const mergedCollection = {
type: "FeatureCollection",
features: mergedFeatures
};
Иногда создание дополнительных потоков невозможно или нецелесообразно.
В этом случае применяется поэтапная обработка через цикл событий.
Пример:
async function processFeatures(features) {
const result = [];
for (let i = 0; i < features.length; i++) {
result.push(
turf.centroid(features[i])
);
if (i % 100 === 0) {
await new Promise(resolve =>
setTimeout(resolve, 0)
);
}
}
return result;
}
Такой подход не создаёт новые потоки, но периодически освобождает основной поток.
Если вычисления независимы друг от друга, можно запускать их параллельно через Promise.
Пример:
const operations = polygons.features.map(
polygon => {
return Promise.resolve(
turf.area(polygon)
);
}
);
const areas = await Promise.all(
operations
);
Следует понимать, что данный подход не создаёт дополнительные потоки. Он лишь упрощает организацию асинхронного кода.
Файл:
// worker.js
const { parentPort } =
require("worker_threads");
const turf = require("@turf/turf");
parentPort.on("message", features => {
const result = features.map(feature =>
turf.centroid(feature)
);
parentPort.postMessage(result);
});
const { Worker } =
require("worker_threads");
Создание:
const worker = new Worker(
"./worker.js"
);
Получение результата:
worker.on("message", result => {
console.log(result);
});
Передача данных:
worker.postMessage(features);
Создание потока для каждой задачи является дорогостоящей операцией.
Поэтому обычно формируется пул.
Пример:
const workers = [];
for (let i = 0; i < 4; i++) {
workers.push(
new Worker("./worker.js")
);
}
Количество потоков часто выбирается исходя из числа доступных ядер процессора.
Некоторые функции практически идеально подходят для параллельной обработки.
Создание буферных зон:
turf.buffer(feature, 5);
Буферы отдельных объектов независимы друг от друга.
Поиск центроидов:
turf.centroid(feature);
Каждый объект вычисляется отдельно.
Расчёт площади:
turf.area(feature);
Отлично масштабируется на несколько потоков.
Определение ограничивающих прямоугольников:
turf.bbox(feature);
Практически не требует синхронизации между потоками.
Упрощение геометрий:
turf.simplify(feature);
Особенно эффективно для сложных полигонов с тысячами вершин.
Разделение линий:
turf.lineChunk(line, 1);
Может обрабатываться параллельно для множества маршрутов.
Некоторые алгоритмы требуют данных из соседних объектов.
Например:
turf.union(poly1, poly2);
или
turf.dissolve(collection);
Подобные операции зачастую зависят от результатов предыдущих вычислений и требуют дополнительной координации между потоками.
Полное распараллеливание таких алгоритмов значительно сложнее.
При использовании Worker-потоков возникает дополнительная стоимость сериализации данных.
Передача большого GeoJSON может занимать заметное время:
worker.postMessage(
hugeFeatureCollection
);
Если коллекция содержит сотни тысяч координат, время передачи становится сопоставимым со временем вычислений.
Поэтому рекомендуется:
Неравномерное распределение данных приводит к простою части потоков.
Плохой вариант:
Worker 1 → 100 объектов
Worker 2 → 100 объектов
Worker 3 → 100 объектов
Worker 4 → 10000 объектов
Три потока быстро завершат работу и будут ожидать четвёртый.
Более эффективный вариант:
Worker 1 → 2575 объектов
Worker 2 → 2575 объектов
Worker 3 → 2575 объектов
Worker 4 → 2575 объектов
Равномерная нагрузка обеспечивает максимальное использование процессора.
Чрезмерное количество Worker-потоков способно ухудшить производительность.
Причины:
Практическое правило:
Количество Worker-потоков
≈
Количество логических ядер CPU
Для большинства настольных систем оптимальным является диапазон от 4 до 12 потоков.
Перед внедрением параллельной обработки полезно измерять реальные показатели.
Пример:
console.time("buffer");
Вычисление:
const result = points.features.map(
feature =>
turf.buffer(feature, 1)
);
Завершение:
console.timeEnd("buffer");
Сравнение результатов до и после распараллеливания позволяет определить фактический выигрыш.
Схема обработки больших геоданных:
Подобная архитектура позволяет эффективно работать с десятками и сотнями тысяч пространственных объектов, сохраняя высокую отзывчивость интерфейса и максимально используя вычислительные ресурсы системы.