Short answer
A vector tile is a small Protobuf (.pbf) packet holding the geometry and attributes of features inside one tile of a zoom pyramid, which the browser then styles and renders itself. That is the key difference from raster tiles, which are pre-rendered PNG/JPEG images with the cartography baked in. Vector tiles (the Mapbox Vector Tile / MVT spec, an OGC Community Standard) are smaller over the wire, restyleable on the client without re-fetching, crisp at any device pixel ratio, and queryable feature-by-feature. You build them with tippecanoe offline or ST_AsMVT from PostGIS, and consume them in MapLibre GL, Mapbox GL, OpenLayers, or deck.gl.
How the tile pyramid works
Web maps use the Web Mercator XYZ tiling scheme (EPSG:3857). The world fits in a single tile at zoom 0; each zoom level subdivides every tile into four, so there are 4^z tiles at zoom z. A tile is addressed by three numbers:
- z — zoom level (0 = whole world, higher = more detail).
- x — column index, counting from the antimeridian (left) eastward.
- y — row index, counting from the top (north) down (the standard "slippy map" / Google convention; TMS flips y).
A client computes which z/x/y tiles cover the current viewport and requests only those. That is why a tiled map scales: a 4K screen at any zoom needs roughly the same number of tiles, regardless of whether the underlying dataset is a city or a continent.
Inside an MVT tile, coordinates are not lat/long — they are integer grid units (the "extent", commonly 4096) local to the tile, which keeps the payload tiny. The renderer maps those grid units back to screen pixels.
Vector vs raster vs GeoJSON
| Property | Raster tiles | Vector tiles | GeoJSON |
|---|---|---|---|
| Payload | Image per tile | Geometry + attributes per tile | Whole dataset at once |
| Restyle without refetch | No | Yes | Yes |
| Interaction / attribute query | No (need separate data) | Yes | Yes |
| High-DPI crispness | Blurs unless retina tiles | Always crisp | Crisp |
| Best for | Imagery, hillshade, continuous rasters | Large/wide-zoom feature data | Small static feature sets |
Use raster tiles for things that are inherently images: satellite imagery, scanned maps, hillshade, continuous coverages. Use GeoJSON when a layer is small (a few thousand features) and you do not need a zoom pyramid. Reach for vector tiles when the data is large, spans many zoom levels, and must stay interactive and themeable — administrative boundaries, road networks, parcels, geological units.
Building vector tiles
Offline with tippecanoe
tippecanoe is the de-facto builder. It expects WGS84 GeoJSON because MVT geometry is derived in Web Mercator from lon/lat:
ogr2ogr -t_srs EPSG:4326 -f GeoJSON layer.geojson layer.gpkg
tippecanoe -o layer.mbtiles \
--minimum-zoom=4 --maximum-zoom=14 \
--layer=features \
--drop-densest-as-needed \
--simplification=4 \
layer.geojson
What the important flags do:
--minimum-zoom/--maximum-zoombound the pyramid. Set max to the real precision of the data; building deeper just inflates tiles and fakes detail.--drop-densest-as-neededremoves features only in tiles that would exceed the size limit, preserving sparse data elsewhere.--simplificationcontrols geometry generalization (Douglas-Peucker); higher = smaller tiles, less detail.--coalesce-densest-as-neededmerges adjacent features to fight tile bloat in dense areas.
The output .mbtiles is a SQLite container of tiles; serve it through a tile server or unpack to a static z/x/y.pbf directory behind a CDN. tippecanoe also emits a TileJSON descriptor that tells the client the layer names, fields, and zoom range.
On the fly from PostGIS
When data changes often, generate tiles per request:
SELECT ST_AsMVT(t, 'features', 4096, 'geom') FROM (
SELECT id, name, category,
ST_AsMVTGeom(
ST_Transform(geom, 3857),
ST_TileEnvelope(:z, :x, :y),
4096, 64, true
) AS geom
FROM features
WHERE geom && ST_Transform(ST_TileEnvelope(:z, :x, :y), 4326)
) t WHERE t.geom IS NOT NULL;
ST_TileEnvelope(z,x,y) produces the tile bounds in 3857; ST_AsMVTGeom clips, simplifies, and quantizes geometry into the tile grid (the 64 is a buffer in tile units so features at edges render without seams); ST_AsMVT serializes the row set. A spatial index on geom (GiST) makes the bounding-box filter fast. Wrap this in a small endpoint that returns the bytes as application/x-protobuf.
Consuming in MapLibre GL
{
"sources": {
"data": { "type": "vector", "url": "https://tiles.example.com/layer.json" }
},
"layers": [
{
"id": "fill",
"type": "fill",
"source": "data",
"source-layer": "features",
"paint": {
"fill-color": ["match", ["get", "category"], "a", "#3b8", "b", "#e83", "#999"],
"fill-opacity": 0.7
}
}
]
}
source-layer must match the layer name baked into the tiles (features above). Because attributes ride along, you can attach click handlers reading feature.properties, filter client-side, and switch the entire theme by swapping a paint expression — no new tile requests. For GPU-heavy rendering of huge datasets, deck.gl's MVTLayer loads the same tiles with accessor functions.
Worked example: 2 million parcels
GeoJSON of 2M parcels would be hundreds of megabytes and crash the browser. With vector tiles:
- Reproject to EPSG:4326, export GeoJSON (or stream from PostGIS).
- tippecanoe at z8-z16; keep only
parcel_id,zone,area_m2as attributes;--drop-densest-as-needed. - At z8 the browser loads a handful of heavily simplified tiles; at z16 it loads only the few tiles over the viewport at full detail. The map stays interactive throughout.
- A click reads
parcel_idfrom the tile and fetches full parcel details from an API — keeping the tiles lean.
Common pitfalls and why they happen
- Wrong CRS input. Feeding projected (UTM) GeoJSON to tippecanoe misplaces everything; tiles assume WGS84 input. Always
-t_srs EPSG:4326first. - Building too deep. Max zoom beyond the data's precision bloats storage and exposes digitizing noise as "detail." Cap it.
- Mismatched
source-layer. A blank map usually means the style'ssource-layerdoes not equal the layer name in the tiles. Check the TileJSON. - Seams at tile edges. Forgetting the
ST_AsMVTGeombuffer (or tippecanoe's clipping) leaves gaps in lines/polygons at boundaries. Use a small tile-unit buffer. - Oversized tiles. Many vertices per feature push tiles past the ~500 KB practical limit and renderers stutter. Pre-simplify (
ST_SimplifyPreserveTopology) or raise--simplification.
Validation checks
- Decode a sample tile (
tippecanoe-decodeor a browser tile inspector) and confirm layer names, zoom range, and attributes. - Compare the tiled layer against the source in QGIS at several zooms; generalization should be invisible at intended scales.
- Watch the network panel: tile counts should stay roughly constant as you pan, and individual tiles should be well under a megabyte.
- Confirm attribute queries return expected values across zoom levels, since size-pressure can drop fields silently.
Bathyl perspective
We choose tiling strategy by data type and scale: raster tiles for imagery and terrain, vector tiles for interactive feature layers, plain GeoJSON only when a dataset is genuinely small. Tiles are built to the data's real precision so a web map is fast without ever implying detail the source does not have.
Related reading
- Vector Tiles for Geological Maps
- Raster Tiles for Terrain Visualization
- Preparing GIS Data for Web Visualization
- WMS vs WFS vs OGC API Features
- Spatial data products