skip to content
Andrei Calazans

React Native Navigation Benchmarks: Deep Diving Where Time Is Spent

/ 5 min read

Part of the series. The intro has the headline numbers — here I go into the traces to explain them.

1 · Why Expo Router costs 3× cold start and RAM

Expo Router is ~2.6× the cold start and ~1.6× the peak RAM of the others. Three compounding causes.

Reanimated/Worklets spins up a second JS runtime

The Expo Router template ships react-native-reanimated@4 and react-native-worklets. None of the other three apps include these.

EvidenceExpo RouterOther three
Reanimated/Worklets native libs in APK8 .so0
Total arm64 .so count2011–18
JS CPU blamed to worklets + reanimated~106 ms/run0

The Worklets package boots a separate Hermes instance for the UI thread. The profile shows [HostFunction] runOnUISync (~70 ms/run), registerCustomSerializable, installTurboModule. This is the main RAM driver — proved below.

The bundle is 2× bigger and 2.6× more modules run at startup

Hermes bytecode bundle (KB)
navigation
1339
rn-navigation
1371
React Navigation
1574
Expo Router
2877
Distinct JS files executed during cold start
React Navigation
36
navigation
38
rn-navigation
43
Expo Router
106

A 2.8 MB bytecode file means more to mmap and evaluate. Expo Router touches 106 source files before the first frame vs 36–43 for the others: React Navigation + the router layer (getRoutes, getStateFromPath, context navigators) + Reanimated/Worklets/Screens.

RAM breakdown by type

LibraryPeak RSSanon heapfile (code)GPUHWUI
rn-navigation18262120523.9
React Navigation19570124502.2
navigation21894124502.2
Expo Router3261761526613.7

Expo Router’s premium is overwhelmingly anonymous heap — JS heaps + the native Worklets allocations.

The two axes have different causes. RAM = Reanimated/Worklets. Cold start = mostly the 2× bundle + 106 modules at boot; Reanimated accounts for only ~10% (~60 ms).

2 · Why rn-navigation is the leanest

The cleanest single signal: RUN_JS_BUNDLE — how long the JS entry takes to evaluate.

RUN_JS_BUNDLE — top-level bundle evaluation (ms, from Systrace)
rn-navigation
55 ms
React Navigation
168 ms
Expo Router
208 ms
navigation
266 ms

rn-navigation evaluates its entry in 55 ms: the JS just registers components and calls Navigation.setRoot(). Tabs and stacks are native Kotlin views — there’s no JS navigation tree to construct, so the first frame arrives fast.

React Navigation is JS-driven on top of react-native-screens. At startup it adds:

  • A real JS navigation tree. NavigationContainer → navigators → screens are React components reconciled by Fabric (completeRoot, createNode, appendChild).
  • Expo module init. ~115 ms of JS is labeled @expo/expo-modules-core by the profiler. But the Expo Tax post shows the fixed Expo overhead is only ~36 ms — the rest is the app’s own native-module traffic wearing an Expo name tag.
  • Theme color processing. color-convert/color-string run resolving the default theme.

The navigation router is interesting in the other direction: it has a native tab bar but the slowest bundle eval (266 ms) and the biggest CREATE_UI_MANAGER_MODULE_CONSTANTS (68 ms vs 11–22 ms). It eagerly requireNativeComponents ~two dozen view managers and reads Material3 constants at import time — heavy JS work before the first frame.

3 · Hot functions at startup (Hermes profile)

JS-thread self-time, averaged over 3 runs.

The shared cold-start tax across all four libraries:

FunctionWhat it is
[HostFunction] getConstantsSynchronous native-module constant loading over JSI — the biggest leaf on Expo apps
getConstantsForViewManagerPer-view-manager config fetch (Fabric registering view types)
completeRoot / createNode / appendChildFabric committing the first UI tree to the native shadow tree
renderWithHooks / beginWorkReact reconciling the initial render
[GC Young Gen] / hadesHermes GC under allocation pressure at boot

Library-specific signatures:

react-native-navigation React Navigation v7 navigation router Expo Router
LibraryJS busy (ms)Where time goes
rn-navigation169react-native-navigation (57) · getConstantsForViewManager · requireNativeComponent
React Navigation219@expo (82) + expo-modules-core (33) · getConstants (74) · @react-navigation render (28) · color-convert
navigation router327getConstants (141!) · view-manager registration · TabBarItem
Expo Router461@expo (163) · react-native-worklets (95) + runOnUISync (70) · expo-modules-core (53) · expo-router (25)

4 · Controlled experiment

The findings above are correlational — Expo Router differs from the others in many ways at once. To isolate Reanimated/Worklets I added only it to rn-navigation (same screens, same navigation, nothing else) and re-measured. (apps/rnn_reanimated_app in the repo.)

rn-navigation (baseline) rn-navigation + Reanimated Expo Router
MetricRNNRNN + ReanimatedΔExpo Router
Cold start (ms)316378+62917
Peak RAM — Flashlight (MB)195320+125308
Peak RSS — trace (MB)182318+136326
of which anon heap (MB)62185+123176
RUN_JS_BUNDLE (ms)55128+73208
Cold start (ms) — Reanimated adds ~62 ms, not ~600
RNN
316
RNN + Reanimated
378
Expo Router
917
Peak RAM (MB) — Reanimated reproduces Expo Router's premium entirely
RNN
195
RNN + Reanimated
320
Expo Router
308

RAM: confirmed. Reanimated alone pushed peak RAM from 195 → 320 MB — matching Expo Router’s 308 MB. The jump is entirely anonymous heap (+123 MB), exactly the Worklets/Hermes UI runtime.

Cold start: mostly not Reanimated. The same change added only +62 ms — about 10% of Expo Router’s ~600 ms gap. The bulk is the 2× bundle and 106 modules at boot.

React Native Navigation Benchmarks