Native Route Object (NRO)
Native Route Object (NRO) is an alternative implementation of the route parser and route model in C++, which enables efficient parsing and efficient storage of route data in native memory.
NRO is designed to address some of the limitations of the Java route object defined in mapbox-java, including:
- high Java heap usage, which is constrained by the per-process Java heap limit Android enforces
- less consistent parsing performance when the Java heap is under pressure
Why adopt NRO
Adopting NRO can reduce route-related Java heap pressure in apps that work with long routes or multiple alternative routes. In these scenarios, route objects can occupy a large share of the Java heap, which increases garbage collection activity, makes route parsing less stable, and in extreme cases can lead to out-of-memory errors.
NRO and the Coordination Layer can improve performance especially well when they are adopted together. Coordination Layer UI components are partially implemented in C++ and can access the native-memory route model more directly than older UI components, which helps reduce Java heap allocations and improve efficiency.
Java Heap Consumption
The following benchmark captures Java heap consumption in a scenario where a test application built a long route by adding waypoint after waypoint through an area with a dense road network. This maximized the size of each route object and forced the Navigation SDK to handle several sets of routes at the same time.
| Approach | Peak (MB) | Average (MB) |
|---|---|---|
| Java Route Model (used by default) | 524 | 311 |
| Native Route Object (NRO) | 314 | 130 |
| NRO with Coordination layer | 109 | 32 |
When Java route objects are used, the Navigation SDK may need to remove current alternative routes before parsing new ones so the route objects fit in memory. When NRO is enabled, alternative routes do not need to be dropped before parsing begins.
Note that the reduction in Java heap usage comes with a trade-off: route data is now stored in native memory instead, so native memory consumption increases by a comparable amount.
Parsing speed
The NRO parser provides more stable parsing performance than the Java parser. When the Java heap is already occupied by the application, garbage collection has to pause the parser more often to reclaim temporary objects.
| Java heap state before benchmark | NRO parsing time (ms) | Java parser time (ms) | NRO speed advantage |
|---|---|---|---|
| nearly empty | 1,360 | 1,520 | +12% |
| 150 MB occupied | 1,345 | 2,483 | +85% |
Enable NRO
Starting from Android Nav SDK 3.20.0, you can enable NRO by providing NavigationOptions with
nativeRouteObject(true).
NavigationOptions.Builder(context)
.nativeRouteObject(true)
// other navigation options
.build()
When you access NavigationRoute#directionsRoute,
you receive a wrapper around the native object, and only the route data your app accesses is copied temporarily to the Java heap.
For most applications, the API change is small. The wrapper is mostly compatible with the Java route object API, but there are a few important nuances to review before rollout.
Compatibility considerations
Route traversal and temporary heap usage
Accessing data from native memory and copying it temporarily to the Java heap:
- increases time to traverse route objects
- creates short spikes in Java memory usage
For example, iterating congestion numeric annotations across all route legs is 6.5× slower with NRO:
routes.forEach {
it.directionsRoute.legs()!!.forEach {
it.annotation()!!.congestionNumeric()!!.forEach { }
}
}
| Approach | Time (ms) | Traversal overhead |
|---|---|---|
| Java Route Model | 2.1 | — |
| Native Route Object (NRO) | 13.5 | 6.5× slower |
This matters most in code that repeatedly walks route legs, steps, intersections, or annotations. If you need to scan all steps or all annotations across the entire route, always do so on a background thread — the full traversal copies a large amount of data from native memory to the Java heap and can block the main thread long enough to drop frames. Wrap these code paths in trace sections so they are visible in Perfetto, and verify that processing time and Java heap usage do not regress significantly after enabling NRO.
Equality and hash behavior
Unlike Java route objects, equals() and hashCode() do not compare object contents. Instead, they compare the native-memory pointers that back each object. This is faster, but equals() can return false and hashCode() can return different values for objects with the same content.
This is especially important after route refreshes. If a refresh produces a new route object without changing the route content, equals() can still return false and hashCode() can still change because the refreshed route is backed by a different native object. The same applies to sub-objects such as voice instructions: even when their content is unchanged after refresh, comparing the old and new objects can still return false.
If your app compares refreshed route objects or their sub-objects, do not assume that unchanged content will preserve equals() or hashCode() results.
Coordinate precision
Geometry-decoding APIs can return Point coordinates whose latitude/longitude values differ from the non-NRO implementation by a small amount. The NRO and non-NRO code paths use different polyline decoders (one written in C++, the other in Java), and small algorithmic differences produce slightly different rounding in the trailing digits. The two values describe the same point on the road to well under a millimeter, but they are not guaranteed to be bit-identical.
This affects any API that returns coordinates derived from the route's encoded polyline geometry — for example, DirectionsRoute.completeGeometryToPoints() (via DecodeUtils) — as well as values computed downstream from them (route line rendering, camera framing, custom map-matching helpers, etc.). Other coordinate-bearing fields that come directly from the JSON response — such as DirectionsWaypoint.location(), StepManeuver.location(), and StepIntersection.location() — and scalar fields such as distance(), duration(), weight(), etc. are not affected.
If your code compares coordinates across the NRO and non-NRO paths (tests, caches, equality checks, etc.), use an epsilon-based comparison rather than exact Double equality.
JSON and builder workflows
Converting route objects and their sub-objects to JSON is not supported with NRO. Rebuilding them through toBuilder() is also not supported.
If your integration serializes route objects, clones them, or patches them on the client side, plan an alternative workflow before enabling NRO.
Client-side route object updates
Features that rely on client-side route object updates are now disabled:
- Automatic cleanup of outdated traffic information in case refresh is not possible
- Client side traffic change
Before enabling NRO, verify that your app does not depend on these behaviors.
Route lifetime and memory retention
With NRO, this is more consequential than with the Java route model. In the Java model, route objects and sub-objects are independent Java objects: keeping a reference to a sub-object such as a voice instruction retains only that instruction in memory, while the rest of the route can be garbage collected. With NRO, sub-objects are views into the same underlying native memory block, so a reference to any one of them — even a small sub-object — keeps the entire native route alive in memory.
For this reason, do not keep references to route objects or sub-objects after the Navigation SDK removes them from the current routes. Although NavigationRoute instances may seem lightweight, they still hold a reference to a large native memory block. The same applies to sub-objects: even a small object such as a voice instruction can prevent the whole native route from being released. Release all references promptly after routes are replaced so native memory can be reclaimed.
Unrecognized properties
Accessing unrecognized properties on route objects is especially inefficient with NRO. Each access triggers a full recursive copy of the accessed field from native memory to the Java heap. For primitive fields this is negligible, but for objects and arrays the copy happens on every access. Avoid accessing unrecognized properties in hot paths, and cache the result if you need to read the same field more than once.
Benchmark conditions
All measurements were collected with automated tests or macrobenchmarks using Navigation SDK version 3.20.0 on a Google Pixel 6 running Android 16.