Tuning
Measure first. ezu --verbose tile … prints per-node op, cache hit/miss, output
shape, and duration, which usually names the culprit before you start guessing.
1. Draw less
Section titled “1. Draw less”The largest wins are almost always in the style, not the runtime.
- Gate layers by zoom.
min-zoom/max-zoomon thefeaturesnode, andmin-zoom-fieldwhere the data carries a per-feature threshold. A minor-road layer drawn at z8 costs real time and is invisible. - Filter early.
filter-expron thefeaturesnode runs once and every consumer benefits. Filtering late, or in several places, does the work twice. - Paint only what is looked at.
fill-dabsandlineare the expensive ops.watercolor.jsonpaints water and roads and uses flatfill-solidfor landmass and landuse — that is a deliberate budget, not a shortcut. - Prefer
stroketolinefor hairlines. At 1 px the brush texture is not visible anyway.
2. Keep the padding tight
Section titled “2. Keep the padding tight”The canvas is tile-size + 2 × pad on each side, so padding costs pixels
quadratically: 512+2×64 is 1.6× the area of 512+2×16. Padding is computed from the
graph, and a $param’s declared max is what gets budgeted — so a generous max
on a blur sigma costs render time on every tile even when the value is small. Keep
bounds as tight as the style needs.
3. Turn on the parallel evaluator
Section titled “3. Turn on the parallel evaluator”ezu = { version = "0.6", features = ["parallel"] }render_parallel fans each depth bucket across Rayon. The win scales with graph
width — a wide painterly graph gains a lot, a deep narrow one gains little.
Two levels exist, and they compete for the same cores:
| Wins on | Use for | |
|---|---|---|
across tiles (ezu tiles --concurrency N) |
throughput | batch pyramid renders |
within a tile (render_parallel) |
latency | a server answering cold requests |
Both at once oversubscribes the CPU. Pick per workload.
4. Make the cache work
Section titled “4. Make the cache work”The cache is content-addressed over (canvas, tile, node param hash + asset hashes, input hashes) and shared across tiles within one style.
- Keep one cache per style for that style’s life. A fresh cache per tile throws away the sharing that makes pyramids cheap.
- Size it deliberately.
Cache::with_capacitycounts entries; a padded 512 px RGBA canvas is about 1.2 MB. - Fan out freely. Referencing one node from five consumers evaluates it once. Duplicating the node five times evaluates it five times.
- Watch for a node that never hits. In
--verboseoutput that is the signal of an assethash()that changes when the data has not — the most common self-inflicted performance bug in a custom host.
5. Cheaper encoding
Section titled “5. Cheaper encoding”PNG compression is a real share of the per-tile wall for simple styles. Lossless
WebP is typically 20–40 % smaller on painterly content at comparable cost, and in
wasm format: 'rgba' skips encoding entirely when the destination is a canvas.
PNG also takes a compression preset — use the fast preset for live previews.
6. In the browser
Section titled “6. In the browser”- SIMD build helps the image-processing ops most.
- Threads build gives you the parallel evaluator across Web Workers, if the page is cross-origin isolated.
- Do not fetch a blind 3×3.
requestedNeighborOffsets(name)is usually empty for a vector source — but bind every offset it does report, DEM and raster included, or the pad comes out clamped and the tile seams. - Cap glyph residency with
setGlyphBudget, and watchmemoryUsage().
7. Fetching, not rendering
Section titled “7. Fetching, not rendering”For translated vector styles at 512 px, HTTP fetch, MVT decode, and PNG encode dominate the per-tile wall clock. If your numbers look worse than the benchmarks and the graph timings look fine, the problem is upstream of ezu: add a tile cache, use PMTiles instead of many small requests, or move the renderer closer to the data.
Profiling deeper
Section titled “Profiling deeper”ezu-cli builds with a heap-profile feature that writes dhat-heap.json,
viewable in dh_view, attributing peak
heap to allocation sites. Reach for it when a render’s memory is not explained by
its pixel buffers.
Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.