Skip to content
Beyond Coordinates
Go back

The Lifecycle of a Mapbox Vector Tile

Updated:

This article is based on “Life of a Tile” from maplibre/maplibre-gl-js. Source links below point to Mapbox GL JS v1.13.2.

The lifecycle of a tile in Mapbox can be divided into three parts:

Ideally, the event and render loops run at 60 frames per second, while expensive work such as tile loading runs asynchronously in Web Workers.


Event Loop

https://github.com/maplibre/maplibre-gl-js/blob/main/docs/diagrams/event-loop.plantuml.svg

Transform

Transform stores the current view state, including pitch, zoom, bearing, and bounds. Two parts of the code update it directly:

Camera and HandlerManager

After changing transform, both Camera and HandlerManager can emit events such as move, zoom, movestart, and moveend. These events—along with style changes, completed data loads, and others—schedule Map#_render() to draw a frame.


Tile Loading

https://github.com/maplibre/maplibre-gl-js/blob/main/docs/diagrams/fetch-tile.plantuml.svg

Map#_render() follows two paths depending on map._sourcesDirty. When it is true, _render() first asks every source whether new data is needed. The false case is covered in the render-loop section.

Tile Selection and Requests

Every source has a corresponding SourceCache. Calling SourceCache#update(transform) determines the tile IDs that should cover the viewport and requests any missing tiles. If the exact tile is unavailable, Mapbox uses a lower-zoom tile covering the same region as a fallback.

Loading and Parsing

Missing tiles are loaded through Source#loadTile(tile, callback). Each source type follows a different path.

RasterTileSource

src/util/ajax issues a getImage request and maintains a queue that limits concurrent requests.

RasterDEMTileSource

Like the raster source, this first requests image data and then sends loadDEMTile to a worker. Reading pixels requires drawing the image to a canvas, an expensive step performed in the worker when OffscreenCanvas is available and on the main thread otherwise.

Inside the worker, RasterDEMTileWorkerSource#loadTile loads raw RGB values into DEMData and fills a one-pixel border to prevent edge flicker. The result is then returned to the main thread.

VectorTileSource

The main thread sends loadTile or reloadTile to a worker. Worker#loadTile receives it and delegates to VectorTileWorkerSource#loadTile.

VectorTileWorkerSource#loadTile then:

The worker calls WorkerTile#parse() and processes the result for the tile ID:

When WorkerTile#maybePrepare() determines that all resources are ready, potpack packs glyphs, icons, and patterns into square atlases suitable for GPU upload. These are stored in GlyphAtlas and ImageAtlas. For each waiting layer, StyleLayer#recalculate() then runs, followed by:

The buckets, feature index, collision boxes, glyph atlas, and image atlas are returned to the main thread.

GeojsonSource

This path is almost identical to VectorTileSource: it sends loadTile or reloadTile to a worker. GeoJSONWorkerSource extends VectorTileWorkerSource but overrides loadVectorData, so it reads GeoJSON directly instead of fetching and parsing PBF vector tiles. geojson-vt converts the data to a tile-like representation, allowing the same getTile workflow to supply the main thread.

This design applies tile-based LOD to GeoJSON and lets the low-level renderer target one vector-tile-like representation instead of maintaining separate paths for vector tiles and GeoJSON.

ImageSource

Mapbox determines a sufficiently high-zoom tile whose bounds contain the ImageSource coordinates. loadTile() returns true only while the main thread requests that tile; the image itself was already requested when a layer using the source was added.


When vector or GeoJSON source data returns to the main thread, Tile#loadVectorData stores it in the tile’s buckets.

Preparing to Render

After the missing tile has loaded, control returns to SourceCache:


Render Loop

https://github.com/maplibre/maplibre-gl-js/blob/main/docs/diagrams/render-frame.plantuml.svg

When _sourcesDirty is false, Map#_render() draws the next frame directly on the main thread:


Share this post:

Previous Post
Publishing Mapbox-Compatible Vector Tiles with GeoServer
Next Post
Three.js Study Notes