Rust / WASM geo
Default stack context: tech-stack skill. This skill covers Rust/WASM geo only.
When to apply
- Turf or JS geometry is too slow (profile first — main-thread blocking > ~100ms)
- OSM parsing, map model build, isochrones, spatial indexes, graph routing
- Thousands of features, repeated geometry ops, or polygon booleans at scale
- Adding or reviewing a
wasm-pack crate, wasm.ts boundary, or Vite WASM plugins
Stay on Turf (JS) for single-feature ops on small GeoJSON and interactive editing feedback during prototyping.
Turf vs Rust/WASM
| Stay on Turf (JS) |
Add Rust/WASM |
| Single-feature ops on small GeoJSON |
Thousands of features, repeated ops, or graph routing |
| Interactive editing feedback |
OSM parsing, map model build, isochrones, spatial indexes |
| Prototyping |
Main-thread blocking > ~100ms (profile first) |
Batch geometry on large arrays (not routing/OSM): geoarrow-wasm instead of rolling your own.
Crate selection (quick)
| Need |
Crate |
| Geometry / GeoJSON |
geo, geojson |
| Spatial index |
rstar |
| Road routing |
fast_paths |
| Graph algorithms |
petgraph |
| OSM → graph |
osm-reader (a-b-street fork) |
| Snap traces to roads |
route-snapper-graph |
| Polygon booleans |
i_overlay (see a-b-street/utils) |
| Contours / isolines |
contour |
| Serialized map models |
bincode + serde |
| Vector file I/O |
flatgeobuf, geozero |
Per-crate usage notes and WASM dependency versions: crates-and-packages.md.
Project structure
One crate per app, named for what it computes (geo-isochrone, route-buffer, …).
app/
├── wasm/
│ └── geo-isochrone/ # Rust crate (wasm-pack output → pkg/)
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # #[wasm_bindgen] surface only
│ └── isochrone.rs # pure geo logic
└── src/
└── features/isochrone/
├── wasm.ts # sole JS import of pkg — React never imports pkg directly
└── useIsochrone.ts # React hook / Query wrapper
lib.rs = #[wasm_bindgen] exports; inner modules = pure geo logic (testable without WASM).
- JS boundary: wrap exports in
wasm.ts. Pass GeoJSON strings or bincode Uint8Array — not hand-built JS objects.
- State split: durable compute state in Rust; UI state in React (Zustand / URL / local
useState).
WASM stack (Cargo)
| Crate |
Role |
wasm-bindgen |
JS ↔ Rust boundary |
serde-wasm-bindgen |
Serialize/deserialize across boundary |
wasm-bindgen-futures |
Async exports |
console_error_panic_hook |
Readable panics in devtools |
getrandom with features = ["js"] |
RNG in browser |
web-time |
Time in WASM |
Build: wasm-pack (--target web), vite-plugin-wasm, target wasm32-unknown-unknown.
Reference setups: a-b-street/ltn, a-b-street/15m, a-b-street/utils.
Docs when stuck: wasm-bindgen book, geo on docs.rs.
App integration
| App type |
Guide |
| Vite SPA (no Start) |
vite-spa-integration.md |
| TanStack Start |
tanstack-start-integration.md |
Both: dynamic-import WASM from wasm.ts only; never top-level import of .wasm in shared modules that may load on the server.
Non-negotiable rules
| Topic |
Rule |
| React imports |
Components/hooks import wasm.ts only — never pkg/ or raw .wasm |
| Boundary types |
GeoJSON as string or bincode Uint8Array; validate with Zod on the JS side after parse |
| Coordinates |
WGS84, [lng, lat] at all API boundaries |
| Server modules |
No WASM init in *.server.ts, server loaders, or createServerFn handlers |
| SSR |
WASM runs client-only; lazy-init after hydration (see Start guide for route ssr) |
| Turf fallback |
Keep Turf for small/interactive paths until profiling proves WASM is needed |
Related skills
| Topic |
Skill |
| Map React API, layers, events |
react-map-gl |
| Loaders, search params, Query |
tanstack-router-conventions |
| Route SSR, Start layout, server |
tanstack-start-conventions |
| Client UI state around map tools |
zustand-state-management |
| useEffect / map listeners |
react-dev |
1---2name: rust-wasm-geo3description: Rust/WASM geo for FixMyBerlin FMC geo-heavy React apps: when to leave Turf for WASM, crate selection (geo, rstar, fast_paths, osm-reader, i_overlay, …), wasm-bindgen boundaries, and Vite integration for SPAs and TanStack Start. Use when adding WASM geo crates, profiling slow Turf, OSM/routing/isochrones, spatial indexes, or wiring wasm-pack into Vite.4---56# Rust / WASM geo78Default stack context: [tech-stack](https://github.com/FixMyBerlin/fixmyskills/tree/main/skills/tech-stack) skill. This skill covers **Rust/WASM geo only**.910## When to apply1112- Turf or JS geometry is too slow (profile first — main-thread blocking > ~100ms)13- OSM parsing, map model build, isochrones, spatial indexes, graph routing14- Thousands of features, repeated geometry ops, or polygon booleans at scale15- Adding or reviewing a `wasm-pack` crate, `wasm.ts` boundary, or Vite WASM plugins1617**Stay on Turf (JS)** for single-feature ops on small GeoJSON and interactive editing feedback during prototyping.1819## Turf vs Rust/WASM2021| Stay on Turf (JS) | Add Rust/WASM |22| ----------------------------------- | --------------------------------------------------------- |23| Single-feature ops on small GeoJSON | Thousands of features, repeated ops, or graph routing |24| Interactive editing feedback | OSM parsing, map model build, isochrones, spatial indexes |25| Prototyping | Main-thread blocking > ~100ms (profile first) |2627**Batch geometry on large arrays** (not routing/OSM): [geoarrow-wasm](https://github.com/geoarrow/geoarrow-js) instead of rolling your own.2829## Crate selection (quick)3031| Need | Crate |32| --------------------- | ------------------------------------------------------------------------- |33| Geometry / GeoJSON | `geo`, `geojson` |34| Spatial index | `rstar` |35| Road routing | `fast_paths` |36| Graph algorithms | `petgraph` |37| OSM → graph | [osm-reader](https://github.com/a-b-street/osm-reader) (a-b-street fork) |38| Snap traces to roads | `route-snapper-graph` |39| Polygon booleans | `i_overlay` (see [a-b-street/utils](https://github.com/a-b-street/utils)) |40| Contours / isolines | `contour` |41| Serialized map models | `bincode` + `serde` |42| Vector file I/O | `flatgeobuf`, `geozero` |4344Per-crate usage notes and WASM dependency versions: [crates-and-packages.md](references/crates-and-packages.md).4546## Project structure4748One crate per app, named for what it computes (`geo-isochrone`, `route-buffer`, …).4950```51app/52├── wasm/53│ └── geo-isochrone/ # Rust crate (wasm-pack output → pkg/)54│ ├── Cargo.toml55│ └── src/56│ ├── lib.rs # #[wasm_bindgen] surface only57│ └── isochrone.rs # pure geo logic58└── src/59 └── features/isochrone/60 ├── wasm.ts # sole JS import of pkg — React never imports pkg directly61 └── useIsochrone.ts # React hook / Query wrapper62```6364- `lib.rs` = `#[wasm_bindgen]` exports; inner modules = pure geo logic (testable without WASM).65- **JS boundary:** wrap exports in `wasm.ts`. Pass **GeoJSON strings** or **bincode `Uint8Array`** — not hand-built JS objects.66- **State split:** durable compute state in Rust; UI state in React (Zustand / URL / local `useState`).6768## WASM stack (Cargo)6970| Crate | Role |71| ------------------------------------ | ------------------------------------- |72| `wasm-bindgen` | JS ↔ Rust boundary |73| `serde-wasm-bindgen` | Serialize/deserialize across boundary |74| `wasm-bindgen-futures` | Async exports |75| `console_error_panic_hook` | Readable panics in devtools |76| `getrandom` with `features = ["js"]` | RNG in browser |77| `web-time` | Time in WASM |7879**Build:** `wasm-pack` (`--target web`), `vite-plugin-wasm`, target `wasm32-unknown-unknown`.8081Reference setups: [a-b-street/ltn](https://github.com/a-b-street/ltn), [a-b-street/15m](https://github.com/a-b-street/15m), [a-b-street/utils](https://github.com/a-b-street/utils).8283**Docs when stuck:** [wasm-bindgen book](https://rustwasm.github.io/docs/wasm-bindgen/), [geo on docs.rs](https://docs.rs/geo).8485## App integration8687| App type | Guide |88| ------------------- | ------------------------------------------------------------------------- |89| Vite SPA (no Start) | [vite-spa-integration.md](references/vite-spa-integration.md) |90| TanStack Start | [tanstack-start-integration.md](references/tanstack-start-integration.md) |9192Both: dynamic-import WASM from `wasm.ts` only; never top-level `import` of `.wasm` in shared modules that may load on the server.9394## Non-negotiable rules9596| Topic | Rule |97| -------------- | ----------------------------------------------------------------------------------------- |98| React imports | Components/hooks import `wasm.ts` only — never `pkg/` or raw `.wasm` |99| Boundary types | GeoJSON as `string` or bincode `Uint8Array`; validate with Zod on the JS side after parse |100| Coordinates | WGS84, `[lng, lat]` at all API boundaries |101| Server modules | No WASM init in `*.server.ts`, server loaders, or `createServerFn` handlers |102| SSR | WASM runs client-only; lazy-init after hydration (see Start guide for route `ssr`) |103| Turf fallback | Keep Turf for small/interactive paths until profiling proves WASM is needed |104105## Related skills106107| Topic | Skill |108| -------------------------------- | ----------------------------- |109| Map React API, layers, events | `react-map-gl` |110| Loaders, search params, Query | `tanstack-router-conventions` |111| Route SSR, Start layout, server | `tanstack-start-conventions` |112| Client UI state around map tools | `zustand-state-management` |113| useEffect / map listeners | `react-dev` |