skip to content
Andrei Calazans

React Native Navigation Benchmarks: What Does Navigating to a Screen Cost?

/ 5 min read

Part of the series. Cold start data doesn’t include any taps, so I captured fresh traces: cold-launch each app, let Home settle, then input tap the shared button while Perfetto Systrace + Hermes record.

Press = startDispatchCycleLocked on system_server (the moment Android hands the tap to the app). Paint = first presented frame from SurfaceFlinger.

I ran this twice — once pushing a trivial Details screen (title + button, ~3 host nodes) and once pushing a 24-row FlatList rendered all at once (~290 nodes). The difference reveals how each library’s architecture holds up under real content.

Trivial screen: press → painted

react-native-navigation React Navigation v7 navigation router Expo Router
LibraryPress → first framePress → fully paintedFramesJanky
rn-navigation44 ms335 ms262
navigation router51 ms330 ms2515
React Navigation62 ms522 ms437
Expo Router61 ms520 ms438
Press → first frame (ms) — lower is more responsive
rn-navigation
44 ms
navigation
51 ms
Expo Router
61 ms
React Navigation
62 ms
Press → fully painted (ms) — lower is better
navigation
330 ms
rn-navigation
335 ms
Expo Router
520 ms
React Navigation
522 ms

All four feel instant — first content appears in 44–62 ms. The ~190 ms gap in fully painted is the transition animation length, not content cost. Expo Router and React Navigation are identical (same 43-frame native-stack slide) because Expo Router renders through React Navigation underneath.

The navigation router’s native Material transition ran 15/25 frames janky on this device despite the short total duration.

What JS is doing during the mount

JS thread busy time to mount Details (ms)
navigation
11.8
rn-navigation
19.5
Expo Router
47.8
React Navigation
79.8

The call stacks tell the architecture directly:

navigation router — 11.8 ms. Pushes a native Scene; JS just commits the content.

11.8ms  [HostFunction] completeRoot   — Fabric commit

rn-navigation — 19.5 ms. Native container, but “did appear” events cross back into JS.

beginWork → renderWithHooks → DetailsScreen        [shared-ui]
triggerOnAllListenersByComponentId                  [react-native-navigation]

Expo Router — 47.8 ms. Fabric commit + React Context fan-out for URL/route state.

24.1ms  [HostFunction] completeRoot
 8.9ms  propagateParentContextChanges
 7.1ms  latestCallback                              [expo-router]

React Navigation — 79.8 ms. Full reconciliation: new screen + navigator + synthetic-event churn.

16.4ms  [HostFunction] completeRoot
10.7ms  [HostFunction] appendChild
11.9ms  beginEvent + updateCallback
 6.7ms  destructor + releasePooledEvent             — event pool

For a trivial screen none of this is a bottleneck. Even React Navigation’s 80 ms of JS overlaps the input-to-frame pipeline — first paint stays under 62 ms for everyone.

Heavy screen: what changes with real content

LibraryJS mountPress → first framePress → fully painted
rn-navigation20 → 87 ms44 → 61 ms335 → 319 ms
navigation router12 → 124 ms51 → 243 ms330 → 526 ms
React Navigation80 → 114 ms62 → 299 ms522 → 724 ms
Expo Router48 → 134 ms61 → 322 ms520 → 749 ms

JS mount jumped for everyone — the leaf is the same across all four: [HostFunction] completeRoot on a ~290-node tree takes ~50–60 ms vs ~12–24 ms trivial. This is a Fabric cost, not a router cost. The navigation router’s “lean-JS” advantage from the trivial screen evaporates entirely.

Press → first frame — heavy screen (ms)
rn-navigation
61 ms
navigation
243 ms
React Navigation
299 ms
Expo Router
322 ms
Press → fully painted — heavy screen (ms)
rn-navigation
319 ms
navigation
526 ms
React Navigation
724 ms
Expo Router
749 ms

rn-navigation barely moved on first frame (61 ms) while the others jumped to 243–322 ms. That’s two different strategies under load:

  • rn-navigation starts the transition immediately, before content is ready. First frame is fast — but it then drops into a 221 ms “App Deadline Missed” frame while the 290-node tree commits. The screen appears, then visibly fills in.
  • The others gate the transition on content. They hold the old screen until the new one renders, so first frame scales with mount cost. Once the animation starts, it’s smooth.
rn-navigation (left) cuts straight to the fully rendered screen while React Navigation (right) is still mid-slide. Same end result, opposite strategy.

On settle time: rn-navigation finishes in ~320 ms (fast but with dropped frames), while the content-gated routers take 526–749 ms (slower to arrive, smoother in motion).

The choice is architectural. rn-navigation optimizes for time-to-first-pixel; the others optimize for a smooth animation. Neither is wrong — they fail differently. The practical fix for heavy screens is the same regardless of router: virtualize the list so the initial commit is a small batch, not 290 nodes at once.

Caveats

  • n = 1 tap per library per screen. First-frame numbers carry ±~15 ms; jank counts are single samples.
  • ”Heavy” = 24 rows committed synchronously (~290 host nodes). A virtualized list would be much cheaper and would largely eliminate the divergence.
  • Transition durations are library defaults — all are configurable.
  • Samsung Galaxy A16, Android 14, Hermes, New Architecture.

React Native Navigation Benchmarks