On-device performance & debug panel for React Native — no Metro, no USB, no computer. One wrapper component, zero dependencies.
Ship it inside your QA / staging build. When a tester hits jank on a release build three timezones away, they open the panel on the phone, see what's slow and why, and share the session as JSON.
npm install react-native-app-inspector
cd ios && pod install # native FPS/CPU/RSS + network capture (optional)react and react-native are the only peer deps. The native module autolinks;
without a rebuild everything still works in JS — you just lose the native
metrics.
import { InspectorRoot } from 'react-native-app-inspector';
export default function Root() {
return (
<InspectorRoot enabled={__DEV__}>
<App />
</InspectorRoot>
);
}That's the whole integration. Capture starts, a draggable FPS / CPU / MB badge floats over the app, and tapping it opens the panel.
| Tab | What it shows |
|---|---|
| Timeline | Time-ordered log of actions, navigation, renders, network, FPS drops, memory and errors. Tap an FPS drop → cause correlation tells you what likely caused it. |
| Network | Every request with method, status, duration. Captured natively (NSURLProtocol / OkHttp interceptor) when the native module is installed, XHR patch otherwise. |
| Taps | Tap→response latency for every pressable, automatically — labels from testID / text, timing from the native touch timestamp to the next presented frame. RAIL-coded: <100 ms good, >300 ms sluggish. |
| Perf | Live JS & UI-thread FPS, CPU, RSS memory, JS heap, jank — plus per-component render stats (count, avg, worst). |
| Screens | Per-screen score 0–100 with the concrete problems: slow load, FPS drops, slow renders, memory growth, slow requests, slow taps. |
| Storage | Key/value browser for AsyncStorage / MMKV / anything: search, pretty-printed JSON, edit, delete, clear. |
| Startup | Time-to-interactive and custom marks (AppInspector.mark('cache-ready')). |
| Settings | Pause live updates, share the session (native share sheet), clear, hide the badge. |
Everything above is captured automatically — network, errors (console.error
/ uncaught), FPS/CPU/memory, renders, taps. You only add calls for what the
library can't see: custom actions and non-React-Navigation screen changes.
All props are optional:
<InspectorRoot
enabled={__DEV__} // false → children render untouched, zero overhead
navigationRef={navRef} // React Navigation ref → automatic screen tracking
storage={AsyncStorage} // persist session → survives a crash/relaunch
storages={[mmkvAdapter(kv)]} // extra stores for the Storage tab
badge={true} // floating FPS badge (drag to any corner)
badgeCorner="bottom-left"
initialTab="timeline"
autoCaptureTaps={true}
profileRoot={true} // root render profiler (id "App")
modules={{ network: true, errors: true, performance: true, slowScreens: true }}
maxEntries={500} // ring-buffer size per feed
/>React Navigation — automatic screen tracking
const navRef = useNavigationContainerRef();
<InspectorRoot enabled={__DEV__} navigationRef={navRef}>
<NavigationContainer ref={navRef}>{/* … */}</NavigationContainer>
</InspectorRoot>;Every screen change is logged, timed and profiled. Without React Navigation,
call AppInspector.trackNavigation('Checkout') yourself or wrap screens in
<InspectorScreen name="Checkout">.
Storage tab — AsyncStorage, MMKV, custom stores
import { asyncStorageAdapter, mmkvAdapter } from 'react-native-app-inspector';
<InspectorRoot
storages={[asyncStorageAdapter(AsyncStorage), mmkvAdapter(new MMKV())]}>Passing storage={AsyncStorage} alone already enables the tab for it. Any
object with { name, getAllKeys, get, set, remove } works as a custom store.
Manual tracking — actions, Redux, network
AppInspector.trackAction('add_to_cart', { sku: 'A-123' });
AppInspector.trackNetwork({ method: 'POST', url: '/orders', status: 201, durationMs: 840 });
// Redux — log every dispatch:
applyMiddleware(AppInspector.getActionLogger().middleware());
// Time a custom interaction (auto tap capture handles normal presses):
const done = AppInspector.beginInteraction('Checkout');
await submitOrder();
done();Profiling one component
<InspectorProfiler id="ProductList">
<ProductList />
</InspectorProfiler>Its commit count / avg / worst render times show up under Perf → Renders.
Exporting a session programmatically
import { exportLogs, shareLogs } from 'react-native-app-inspector';
const json = exportLogs(); // full snapshot as a JSON string
await shareLogs(); // native share sheet (also in the Settings tab)AppInspector.getPreviousSession() returns the persisted previous session
(set storage to enable) — including one that ended in a crash.
Custom UI instead of the badge
<InspectorRoot badge={false} /* … */>
<App />
</InspectorRoot>Then render <InspectorModal visible={open} onClose={…} /> from your own
trigger (shake, hidden multi-tap, dev menu). useInspectorState() gives you
the live state for fully custom UIs.
Prefer rendering it as a sibling of
InspectorRoot— inside the profiled subtree, commits containing the open panel are excluded from the render stats.
example/ is a Todo app wired for inspection — real network,
actions, renders and taps to look at:
cd example && npm install && npm run ios # or npm run androidnpm run typecheck && npm run lint && npm testProject structure
src/
core/ controller, observable store, shared types (no react-native)
modules/ timeline, performance, screens, render, startup, network,
actions, errors, interactions, taps, navigation, storage,
persistence, deviceInfo
native/ bridge to the iOS/Android native module
ui/ badge, modal, tabs
export/ snapshot + serialization
ios/ android/ native metrics + network capture
MIT © phaelor






