React Native Navigation Benchmarks: What's the Expo Tax?
/ 3 min read
Part of the series. The deep dive showed the Hermes profiler attributing up to 241 ms of JS-thread CPU to @expo/expo-modules-core at startup. Here I run the actual control to separate the real Expo overhead from the noise.
Setup: two minimal apps rendering the identical Home screen — no navigation, no profiling instrumentation.
bare-min: bare RN 0.85, zero Expo.expo-min: Expo SDK 56 blank app. Cold start = OSDisplayed; RAM =dumpsys meminfoPSS at Home. Median of 5 runs. (experiments/in the repo.)
What every Expo app ships
Even a blank Expo app autolinks 10 native modules — the same 10 appear in all three Expo apps in the main comparison:
expo · expo-modules-core · expo-asset · expo-constants · expo-file-system · expo-font · expo-keep-awake · expo-status-bar · @expo/dom-webview · @expo/log-box
| App | Expo deps declared | Native Expo modules autolinked |
|---|---|---|
| expo-min (blank) | 1 | 10 |
| React Navigation | 2 | 10 |
| navigation router | 2 | 10 |
| Expo Router | 9 | 15 |
| bare RN apps | 0 | 0 |
The fixed Expo core tax
| Metric | bare-min | expo-min | Expo tax |
|---|---|---|---|
Cold start — Displayed (ms) | 293 | 329 | +36 |
| RAM — PSS at Home (MB) | 67.0 | 79.6 | +12.6 |
| APK size (MB) | 49.5 | 65.7 | +16.2 |
~36 ms, ~13 MB RAM, ~16 MB APK. That’s the cost of expo-modules-core plus the 10 baseline modules. For most apps that’s a reasonable price.
What each extra module adds
Starting from expo-min, I added one module at a time — installed and autolinked, but not actually used.
| Added module | Δ cold start | Δ RAM PSS | Class |
|---|---|---|---|
expo-device | +3 ms | ~0 | constants only |
expo-haptics | +3 ms | ~0 | methods only |
expo-clipboard | ~0 | +0.2 MB | methods only |
expo-linear-gradient | ~0 | +0.2 MB | native view |
expo-image | ~0 | +1.7 MB | native view + Glide |
expo-blur | +12 ms | +0.5 MB | native view |
expo-notifications | +11 ms | +2.7 MB | +1 transitive module |
expo-video | +16 ms | +5.9 MB | media3 SDK |
expo-camera | +25 ms | +5.1 MB | CameraX SDK |
Simple modules (device, haptics, clipboard) are essentially free at idle. Even native-view modules add only 0–2 MB. Only heavy native SDKs — Camera (CameraX), Video (media3) — show a meaningful cost, and even then it’s ~15–25 ms and ~5–6 MB while unused. expo-modules-core initializes lazily, so the registry itself is cheap.
Cold-start deltas for light modules are inside run-to-run noise (±~15 ms on this device). PSS is the more reliable per-module signal.
Why the profiler blames 100–240 ms on @expo
The deep dive showed the profiler attributing large JS-thread CPU to @expo:
| App | JS CPU blamed to @expo + expo-modules-core |
|---|---|
| React Navigation | ~118 ms |
| navigation router | ~185 ms |
| Expo Router | ~241 ms |
These numbers scale with each app’s total native-module traffic, not with a fixed Expo overhead. In an Expo app every native-module call goes through expo-modules-core’s registry, so the profiler labels the app’s own UIManager calls, getConstants calls, and view-manager fetches as @expo. The same calls in a bare RN app show up as react-native/Native. The part that is truly extra — the registry/proxy indirection itself — is the ~36 ms measured above.
React Native Navigation Benchmarks