Web Workers

В библиотеке MapLibre GL JS ключевая часть производительности основана на разделении обязанностей между главным потоком браузера и фоновыми потоками выполнения. Основная идея заключается в том, чтобы максимально разгрузить UI-thread и перенести тяжёлые операции — парсинг данных, подготовку геометрии, обработку тайлов и часть вычислений рендеринга — в отдельные воркеры.

Параллелизм достигается за счёт использования Web Workers, которые позволяют выполнять JavaScript-код в изолированных потоках без доступа к DOM.

В контексте MapLibre GL JS это особенно важно, так как рендеринг карт включает:

  • декодирование векторных тайлов
  • триангуляцию геометрии
  • интерполяцию свойств стилей
  • подготовку буферов для WebGL
  • управление кэшем ресурсов

Все эти операции потенциально блокируют главный поток при высоких нагрузках.


Разделение потоков: main thread и worker thread

Архитектура MapLibre GL JS строится вокруг двух ключевых компонентов:

Главный поток

Главный поток отвечает за:

  • обработку пользовательского ввода (pan, zoom, rotate)
  • управление состоянием карты
  • применение стилей
  • взаимодействие с DOM
  • управление lifecycle карты

При этом он не выполняет тяжёлую геопространственную обработку.

Worker поток

Worker выполняет:

  • загрузку и парсинг векторных тайлов
  • преобразование геометрии в WebGL-совместимые структуры
  • вычисление слоёв стиля
  • подготовку данных для рендера
  • оптимизацию батчинга отрисовки

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


Поток данных между main thread и Web Worker

Связь между потоками реализуется через message passing. Основной механизм — postMessage и onmessage.

Общий принцип обмена

  1. Главный поток отправляет запрос на обновление карты.
  2. Worker получает команду и обновляет состояние сцены.
  3. Worker возвращает готовые render-payload структуры.
  4. Главный поток передаёт их в WebGL контекст.

Пример упрощённой коммуникации

// main thread
worker.postMessage({
  type: 'update',
  style: styleJSON,
  zoom: map.getZoom()
});

worker.onmess age = (e) => {
  const { buckets } = e.data;
  render(buckets);
};

Что именно делает Worker в MapLibre GL JS

1. Обработка векторных тайлов

Векторные тайлы приходят в сжатом формате (Mapbox Vector Tile). Worker:

  • декодирует protobuf-структуры
  • распаковывает геометрию
  • преобразует координаты в экранные пространства
  • нормализует свойства фич

Эта операция является одной из самых тяжёлых в pipeline.


2. Сборка слоёв (layer evaluation)

Стиль карты описывает множество слоёв, каждый из которых имеет:

  • фильтры
  • источники данных
  • правила отрисовки

Worker вычисляет:

  • какие фичи попадают в слой
  • какие свойства применяются
  • какие выражения стиля должны быть вычислены

Особенно затратны expression-вычисления (interpolate, match, step).


3. Подготовка буферов для WebGL

Перед передачей на GPU данные проходят этап буферизации:

  • формирование vertex buffers
  • создание index buffers
  • упаковка атрибутов (color, width, offset)
  • оптимизация batch-отрисовки

Эти данные затем используются главным потоком для рендеринга через WebGL API.


Message protocol внутри MapLibre GL JS

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

Основные типы сообщений:

  • loadTile — загрузка тайла
  • parseTile — декодирование данных
  • updateLayers — пересчёт стиля
  • removeTile — удаление из памяти
  • abortTile — отмена загрузки

Каждое сообщение содержит минимальный набор данных, часто с использованием Transferable Objects.


Transferable Objects и оптимизация памяти

Одной из ключевых оптимизаций является использование transferable объектов:

  • ArrayBuffer
  • ImageBitmap
  • OffscreenCanvas

При передаче между main thread и worker данные не копируются, а передаются по ownership.

Это критично для производительности при работе с:

  • большими геометриями
  • плотными городскими слоями
  • 3D terrain data (в расширенных конфигурациях)

Кэширование в worker-слое

Worker управляет несколькими уровнями кэша:

Tile cache

Хранит уже обработанные тайлы, чтобы избежать повторного парсинга.

Bucket cache

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

Glyph cache

Шрифтовые глифы кешируются для ускорения текстового рендеринга.


Поток рендеринга: от данных к WebGL

Pipeline в MapLibre GL JS можно описать как последовательность этапов:

  1. Fetch tiles
  2. Decode in worker
  3. Style evaluation
  4. Bucket creation
  5. Transfer to main thread
  6. WebGL draw calls

Worker выполняет шаги 2–4, главный поток — 5–6.


Web Workers и многослойность стиля

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

Важные особенности:

  • порядок слоёв сохраняется
  • фильтры вычисляются в worker
  • зависимости между слоями учитываются через dependency graph
  • layout properties частично кэшируются

Параллелизм и производительность

Использование Web Workers позволяет:

  • избегать блокировки UI при зуме и панорамировании
  • распределять нагрузку на многоядерные CPU
  • обрабатывать большие наборы геоданных без лагов

Однако существуют ограничения:

  • невозможность доступа к DOM
  • стоимость сериализации сообщений
  • ограничение на shared memory в стандартных сценариях

Ограничения архитектуры worker-based рендеринга

Несмотря на эффективность, модель имеет узкие места:

1. Message passing overhead

При частых обновлениях стиля возрастает нагрузка на сериализацию.

2. Сложность синхронизации

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

3. Ограничения дебага

Ошибки в worker сложнее диагностировать из-за изоляции окружения.


Инкрементальные обновления

MapLibre GL JS использует подход incremental updates:

  • обновляются только изменённые тайлы
  • пересчитываются только затронутые слои
  • минимизируется пересоздание буферов

Worker отслеживает dirty state для оптимизации перерасчётов.


Управление задачами внутри worker

Внутри worker используется очередь задач:

  • high priority: взаимодействие пользователя (pan/zoom)
  • medium: загрузка тайлов
  • low: prefetch и кэширование

Такой приоритетный планировщик позволяет сохранять отзывчивость интерфейса.


Взаимодействие с WebGL через главный поток

Worker не имеет доступа к WebGL напрямую. Поэтому:

  • вся GPU-работа остаётся в main thread
  • worker подготавливает только данные
  • rendering engine получает уже готовые buffers

Это архитектурное решение минимизирует сложность и повышает переносимость.


Будущее оптимизаций worker-архитектуры

Векторные направления развития:

  • расширение использования OffscreenCanvas
  • внедрение SharedArrayBuffer для снижения копирований
  • более агрессивная параллелизация tile parsing
  • частичная миграция рендеринга в worker (гибридные модели)

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