Vector vs Raster Tile Tradeoffs

Choosing between vector .pbf and raster .png/.webp tiles is a systems engineering decision, not a visual preference. The choice cascades through every downstream stage: tile generation compute, MBTiles SQLite container sizing, CDN cache TTL strategy, client-side GPU budget, and style update cadence. This page dissects each dimension with measurable parameters and ends with a reproducible evaluation workflow.

Prerequisites

Before running the evaluation workflow, confirm:

  • GDAL/OGR ≥ 3.6 (gdal-config --version) — needed for gdal2tiles.py raster generation
  • Tippecanoe ≥ 2.17 (tippecanoe --version) — vector tile generation; see essential Tippecanoe flags for flag semantics
  • Python ≥ 3.10 with requests, rasterio, and psutil installed
  • Source data: a GeoJSON or .geoparquet file for vector; a GeoTIFF (EPSG:4326) for raster
  • A staging tile server serving both .pbf (e.g., martin) and .png endpoints
  • k6 ≥ 0.50 for load simulation

Format Comparison at a Glance

The SVG below maps the two delivery paths from source data to browser render. Annotated points show where each format introduces irreversible decisions.

Vector against raster on the dimensions that decide the architectureSeven rows comparing what ships in the tile, restyling, client cost, server cost, interactivity, offline use and imagery support.AT A GLANCEVector tilesRaster tilesWhat the tile holdsGeometry and attributesFinished pixelsRestyle withoutrebuildingYes, at runtimeNo, re-rendereverythingClient costParse plus GPU drawBlit the imageTiles to storeOne set, any styleOne set per styleFeature click and hoverBuilt inNeeds a separate queryHigh-DPI screensFree — vectors rescale2x tiles, 4x the bytesSatellite imageryNot applicableThe only option
The first row causes all the others. Once styling moves to the client, both the wins and the costs follow from it.

Core Tradeoff Dimensions

Bandwidth and Transfer Size

Bytes on the wire per tile, by zoomMean transfer size per tile from zoom 6 to zoom 16 for gzipped vector tiles, PNG raster tiles and WebP raster tiles over the same urban extent.MEASURED240180120600z6z8z10z12z14z16zoom level, same urban extentVector, gzipped (KB)Raster PNG (KB)Raster WebP (KB)
Vector wins at low zoom by a wide margin and converges with WebP at high zoom, where geometry density catches up with pixel entropy. The saving is not uniform, so measure at the zooms your readers actually use.
Metric Vector (.pbf) Raster PNG Raster WebP
Typical z14 urban tile 15–55 KB 60–140 KB 30–80 KB
Tile size at z10 (sparse) 2–8 KB 10–40 KB 5–20 KB
Scales with attribute density Yes No (pixel-bound) No
Compressible on CDN Yes (gzip ~30% gain) Minimal (already binary) Minimal
Max safe tile budget 500 KB 500 KB 500 KB

Vector tiles compress geometry via Protocol Buffers, then further via gzip/brotli at the HTTP layer. Raster tiles are already pixel-compressed — PNG uses DEFLATE, WebP uses lossy/lossless discrete cosine transforms. The vector advantage narrows for dense attribute schemas (many string properties per feature) and widens for sparse datasets like road networks.

Styling Flexibility

Raster tiles bake cartographic decisions at generation time. Changing a road colour, adjusting label hierarchy, or switching to a dark theme requires regenerating the entire tileset or maintaining parallel raster caches per theme. Vector tiles decouple data from presentation: the MapLibre GL JSON style controls colour, font, icon, and visibility at runtime. This makes data-driven property binding — e.g. colouring features by a numeric attribute — trivial in vector but impossible in raster without server-side re-render.

Server Compute and Caching

Raster generation is CPU-intensive during pre-processing (resampling, reprojection, tile cutting) but zero-cost at request time — the server reads a file and streams bytes. Vector generation via Tippecanoe’s simplification and attribute filtering is lighter per tile but produces outputs that change when the source data or style schema changes.

Cache TTL implications differ sharply:

Dimension Vector tiles Raster tiles
Cache-Control Shorter TTL or versioned URLs when style/data changes max-age=31536000, immutable safe for static datasets
Cache busting strategy Increment tile URL version prefix on data rebuild Content-hash suffix on source GeoTIFF → full retile
CDN edge cache fill Slower (many small .pbf files) Faster (fewer, larger PNG files)
Partial update Retile only changed z/x/y tiles Usually full retile (raster continuity matters)

Client-Side Rendering Overhead

Which format fits which mapSix map types plotted against how much styling changes at runtime and how much raw imagery the map must show.WHERE EACH FITSimagery content →runtime restyling →11Data dashboard recoloured per filter22Light and dark basemap from onebuild33Navigation map with live traffic44Hybrid: imagery under vector labels55Satellite imagery viewer66Scanned historical map sheets
The top-left corner is vector's home ground and the bottom-right is raster's. Most real basemaps live on the diagonal and ship both.

Raster rendering is a DOM/canvas image draw — predictable across devices but aliased at non-native zoom levels. Vector rendering requires a WebGL context, geometry parsing, style evaluation, and on-GPU rasterisation per frame. On low-end Android devices (≤ 2 GB RAM, Adreno 300-series GPU), complex polygon layers with fill-extrusion or dense symbol layers can drop to sub-20 fps. Profile GPU draw calls using Chrome DevTools > Performance > Layers before committing vector tiles to broad consumer audiences.


Step-by-Step Evaluation Workflow

Step 1 — Collect Baseline Transfer Metrics

python
# benchmark_tiles.py  — measure transfer size and latency per format per zoom
import time
import csv
import requests

VECTOR_URL = "http://localhost:7800/tiles/{layer}/{z}/{x}/{y}"
RASTER_URL = "http://localhost:8080/tiles/{z}/{x}/{y}.png"

# Representative sample: z10 sparse, z14 urban, z16 dense
SAMPLES = [
    {"z": 10, "x": 542, "y": 359},
    {"z": 14, "x": 8682, "y": 5741},
    {"z": 16, "x": 34732, "y": 22966},
]

results = []
for s in SAMPLES:
    for fmt, url_tpl in [("vector", VECTOR_URL), ("raster", RASTER_URL)]:
        url = url_tpl.format(layer="roads", **s)
        t0 = time.perf_counter()
        r = requests.get(url, timeout=10)
        elapsed_ms = round((time.perf_counter() - t0) * 1000, 1)
        results.append({
            "format": fmt,
            "zoom": s["z"],
            "size_kb": round(len(r.content) / 1024, 2),
            "latency_ms": elapsed_ms,
            "status": r.status_code,
        })

with open("tile_benchmark.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=results[0].keys())
    writer.writeheader()
    writer.writerows(results)

print("Benchmark saved to tile_benchmark.csv")

Verify: confirm size_kb for z14 vector tiles stays under 500 KB. Tiles above this threshold cause client decode stalls and should trigger attribute dropping.

Step 2 — Run Automated Generation Pipelines

Both commands must target identical geographic extents and zoom ranges for a fair comparison.

bash
# Raster generation — GeoTIFF → PNG tile pyramid, zoom 10–16
gdal2tiles.py \
  --processes=4 \
  --zoom=10-16 \
  --resampling=lanczos \
  --profile=mercator \
  --tilesize=256 \
  --srcnodata=0 \
  source_EPSG4326.tif \
  ./output/raster/

# Vector generation — GeoJSON (EPSG:4326) → .mbtiles, zoom 10–16
tippecanoe \
  --output=./output/vector/roads.mbtiles \
  --layer=roads \
  --minimum-zoom=10 \
  --maximum-zoom=16 \
  --drop-densest-as-needed \
  --extend-zooms-if-still-dropping \
  --coalesce-densest-as-needed \
  --simplification=10 \
  source.geojson

Verify: the .mbtiles file opens cleanly with sqlite3 roads.mbtiles "SELECT count(*) FROM tiles;". If applying geometry simplification, run tippecanoe-decode on a representative tile to confirm vertex counts are reasonable.

Step 3 — Simulate Concurrent Cache Load

js
// k6_tile_load.js  — run with: k6 run k6_tile_load.js
import http from "k6/http";
import { sleep, check } from "k6";

export const options = {
  vus: 50,
  duration: "30s",
};

const TILES = [
  "http://localhost:7800/tiles/roads/14/8682/5741",  // vector
  "http://localhost:8080/tiles/14/8682/5741.png",    // raster
];

export default function () {
  TILES.forEach((url) => {
    const res = http.get(url);
    check(res, {
      "status 200": (r) => r.status === 200,
      "under 500KB": (r) => r.body.length < 512000,
    });
  });
  sleep(0.1);
}

Inspect the k6 summary for http_req_duration p95 — raster should show lower server-side duration (file read) but higher transfer time; vector should show the inverse. CDN cache-hit ratios for vector endpoints drop sharply when style versioning causes URL churn; verify Cache-Control response headers match your intended TTL strategy.

Step 4 — Apply the Decision Matrix

Use case Recommended format Rationale
Satellite / aerial imagery basemap Raster (WebP) Pixel fidelity paramount; no styling needed
Interactive POI filtering, dynamic theming Vector Runtime attribute access and style expression binding
Low-bandwidth / emerging markets Raster (WebP/AVIF) Predictable decode; no WebGL dependency
High-density urban routing, transit Vector Topology preservation; crisp lines at all zoom levels
Offline mobile app Vector + PMTiles Single-file; HTTP range-request delivery; no tile server
Print cartography Raster (high-DPI PNG) Pixel-accurate output; no WebGL required
Multi-language label rendering Vector Labels stored as attributes, styled at runtime per locale

The Cost That Does Not Appear in Any Table

Comparisons of these two formats tend to measure bytes, storage and render time, all of which favour one side or the other depending on the map. The cost that decides most real projects is not in that list: it is what happens when the design changes.

With raster tiles, a new brand colour, a darker theme, a wider road casing or a relabelled category is a re-render of the entire pyramid. That is hours to days of compute, a full re-upload, and a cache that goes cold everywhere at once. The change is technically trivial and operationally a project, which is why raster basemaps tend to freeze visually — the cost of changing them exceeds the value of most changes.

With vector tiles the same change is a style deploy: a new JSON document, a sixty-second TTL, and no tile is touched. This is why teams that adopt vector tiles end up iterating on cartography in a way they did not before. It is not that the format is prettier; it is that the feedback loop went from days to seconds, and work expands into whatever loop is short enough to be worth doing.

The mirror image is worth stating too. If a map’s appearance is genuinely fixed — a scanned historical series, a satellite basemap, a print-derived product — that flexibility buys nothing, and the client-side cost of parsing and drawing geometry is paid on every device for a capability nobody will use.

Optimization Knobs

1 — Tile Size vs Attribute Richness (Vector)

--simplification value Geometry quality Avg tile size change
4 (default) Near-lossless for z14+ Baseline
10 Visible simplification at z16 −20 to −35%
20 Acceptable for z10–12 overview −40 to −55%

Combine with attribute filtering rules — dropping even two unused string properties can cut tile size by 15–25% on feature-dense layers.

2 — Raster Compression vs Visual Quality

Format Typical z14 size Decode speed Lossy artefacts
PNG-8 25–50 KB Fast None (palette quantisation)
PNG-32 60–140 KB Fast None
WebP lossy (q=80) 15–40 KB Moderate Subtle at label edges
WebP lossless 30–80 KB Slower None

Pass --webp to gdal2tiles.py (GDAL ≥ 3.1) to produce .webp tiles; serve with Content-Type: image/webp.

3 — Zoom Level Strategy

For either format, every additional zoom level roughly quadruples the tile count. Limiting maximum zoom to the dataset’s natural resolution avoids generating tiles where vector geometry is over-simplified or raster pixels are upscaled. As a rule of thumb: 1 m/px imagery supports z18 at most; 10 m/px imagery degrades past z16.


Integration with Adjacent Pipeline Stages

Vector output → tile server → CDN. After generating .mbtiles, use martin or tileserver-gl to serve tiles with appropriate Content-Encoding: gzip headers. For static hosting, convert to PMTiles format with pmtiles convert roads.mbtiles roads.pmtiles and upload to Cloudflare R2 or S3 — the PMTiles HTTP range-request specification allows the browser to fetch individual tiles without a server process.

Raster output → S3/R2 directory → CDN. Raster tile directories map directly to static object storage. Set Cache-Control: public, max-age=31536000, immutable on .png/.webp objects. When you regenerate tiles, rotate the URL version prefix (/tiles/v3/…) rather than purging the CDN edge cache.

Style coupling. Vector tiles require a MapLibre GL style JSON that references the correct tile source URL and layer names. Version the style JSON independently of the tile archive — a style update does not require a tile retile, but a layer rename does.


Troubleshooting

1 — Vector tiles exceed 500 KB at high zoom

Symptom: tippecanoe logs tile is too large; client shows blank tiles at z16+.

Diagnosis:

bash
tippecanoe-decode roads.mbtiles 16 34732 22966 | python3 -c "
import sys, json
data = json.load(sys.stdin)
for layer in data['features']:
    print(layer['geometry']['type'], len(json.dumps(layer)))
"

Fix: Add --drop-densest-as-needed and reduce --simplification below 4 (counter-intuitively, a higher number simplifies more aggressively). Also drop unused attributes before generation.

2 — Raster tiles show seam artefacts at tile boundaries

Symptom: Thin white or dark lines at 256-pixel intervals on the map.

Diagnosis: Compare adjacent tiles — consistent one-pixel borders indicate padding (--expand in gdal2tiles.py or NODATA bleed-through).

Fix:

bash
gdal2tiles.py --zoom=10-16 --resampling=lanczos --srcnodata="0 0 0" source.tif ./output/raster/

Set --srcnodata to the exact background pixel value to prevent transparent halo bleed.

3 — CDN serves stale vector tiles after data update

Symptom: Clients see outdated geometry after a rebuild; hard refresh fetches updated tiles.

Fix: Use versioned URL paths and increment on rebuild:

bash
# In your build script
VERSION=$(date +%Y%m%d%H%M)
tippecanoe -o roads_${VERSION}.mbtiles ...
# Upload to /tiles/v${VERSION}/{z}/{x}/{y}.pbf
# Update style JSON source URL to reference new version

Never rely on Cache-Control: no-cache for tile endpoints under CDN load — origin fetch costs compound rapidly at scale.

4 — WebGL context lost on mobile

Symptom: MapLibre map goes blank; console shows WebGL context lost.

Diagnosis: Device exhausted GPU memory. Check layer count and zoom range in the style JSON.

Fix: Reduce concurrent rendered layers, increase maxTileCacheSize in MapLibre options, or fall back to a raster basemap at zoom levels below z12 where vector geometry is sparse.

5 — Projection mismatch (EPSG:4326 vs EPSG:3857)

Symptom: Tiles render in the wrong location; coordinates are off by hundreds of kilometres.

Diagnosis:

bash
gdalinfo source.tif | grep "AUTHORITY"
# Should return AUTHORITY["EPSG","4326"] for input; tiles are reprojected to 3857 during generation

Fix: If your source GeoTIFF is already in EPSG:3857, pass --profile=mercator explicitly to gdal2tiles.py. For GeoJSON, Tippecanoe expects EPSG:4326 input and handles the Web Mercator reprojection internally.


FAQ

Is one format simply better than the other?

No. Vector wins wherever styling changes or features are interactive; raster wins wherever the content is imagery. Most production basemaps ship both and hand over between them by zoom.

Do vector tiles always transfer fewer bytes?

At low zoom, by a wide margin. At high zoom, geometry density catches up with pixel entropy and vector converges with WebP. Measure at the zooms your readers actually use.

What is the client-side cost of vector tiles?

Parsing plus GPU drawing, against a raster tile’s simple blit. On low-end mobile hardware this is the main argument for raster, and it is why a hybrid basemap is often the pragmatic answer.

Can I convert an existing raster tileset to vector?

Not meaningfully. Raster tiles are finished pixels with no geometry or attributes to recover. Producing vector tiles means going back to the source data.

Further Reading


Next reading When to Use Vector Tiles Over Raster for Web Maps