Last Minute Notes

Performance and Debugging

React Native performance and debugging: how to measure first, frame drops on the JS and UI threads, unnecessary re-renders, React.memo and the React Compiler, list and image performance, startup time, bundle size, memory leaks, Android-specific issues, React Native DevTools, native profilers, and crash and error monitoring.

📘 Performance and Debugging

"Our app is slow, how would you fix it?" is the senior RN interview question. The expected answer starts with measuring in a release build on a low-end device, then identifying which thread is the bottleneck, then applying targeted fixes. This page is that process, plus the debugging tools you will use daily.

Table of Contents

  1. Measure First
  2. Which Thread Is Slow
  3. Re-renders
  4. The React Compiler
  5. Lists and Images
  6. Startup Time
  7. Bundle and App Size
  8. Memory Leaks
  9. Android-Specific Performance
  10. Debugging Tools
  11. Crashes and Error Monitoring
  12. Performance Checklist
  13. Questions

1. Measure First

  • Release builds only. Development mode adds prop-type style checks, warnings, Fast Refresh, and unminified code; it can be several times slower.
  • Low-end Android device. A three-year-old budget Android phone is where problems show; the iOS simulator on a laptop hides almost everything.
  • Define metrics before optimizing:
MetricWhat it measuresTooling
TTI / cold startLaunch to usable first screenNative markers, react-native-performance, Android vitals, Xcode Organizer
FPS / frame dropsSmoothness on JS and UI threadsPerf monitor overlay, Perfetto, Instruments
Screen transition timeTap to next screen renderedCustom markers, Flashlight
MemoryPeak and growth over a sessionXcode Memory Graph, Android Studio profiler
App sizeDownload and install sizeStore reports, size analyzers
ANR / crash rateStabilityPlay Console, Sentry, Crashlytics

Flashlight gives a Lighthouse-style performance score for Android apps and works well in CI to catch regressions.


2. Which Thread Is Slow

JS thread problemsUI thread problems
Re-rendering large trees on every keystrokeDeep view hierarchies, many nested Views
Expensive computation in renderOverdraw: stacked opaque backgrounds, transparency
Parsing large JSON responsesDecoding large images on the main thread
Synchronous storage reads on hot pathsMany shadows (especially on Android)
Too many timers, intervals, listenersAnimations of layout properties
Logging (console.log) in hot loops in productionText-heavy layouts re-measured often

Strip console.* calls from release bundles with babel-plugin-transform-remove-console or a logger that no-ops in production.


3. Re-renders

Re-renders cost more on mobile because the JS thread is shared with all app logic and devices are slower.

Common causes and fixes:

CauseFix
Parent state change re-renders all childrenMove state down, split components, React.memo
New object, array, or function props each renderStable references (useMemo, useCallback) or the React Compiler
Big Context value changesSplit contexts, or use a store with selectors (Zustand, Redux)
Subscribing to a whole storeSelect the minimum: useStore((s) => s.cart.count)
Storing high-frequency values in state (scroll position, animation progress)Shared values or refs instead of state
Hidden screens re-renderingfreezeOnBlur in React Navigation

Find them with the React DevTools Profiler in React Native DevTools ("Highlight updates when components render", and "Why did this render?" in the flamegraph).


4. The React Compiler

The React Compiler is a Babel plugin that automatically memoizes components and hooks at build time.

  • It analyzes code that follows the Rules of React and inserts fine-grained memoization, so most manual useMemo, useCallback, and React.memo become unnecessary.
  • Supported on React Native and enabled by default in new Expo projects in recent SDKs.
  • It skips components that break the Rules of React (mutating props, reading refs during render); the ESLint plugin reports these.
  • It does not fix algorithmic problems, huge lists without virtualization, or slow native views.
// babel.config.js (bare RN)
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['babel-plugin-react-compiler'],   // must run first
};

Interview angle: "Do you still need useCallback?" Mostly no with the compiler, but you need to understand why it exists (reference equality for memoized children, effect dependencies) to debug the cases the compiler skips.


5. Lists and Images

Lists: see Lists, State, and Data for virtualization tuning, memoized rows, and FlashList.

Images are the most common cause of memory crashes:

ProblemFix
Downloading 4000 px images for 100 pt thumbnailsRequest resized images from a CDN (width parameter matching layout times pixel ratio)
No cachingexpo-image with memory and disk cache
Decoding many large images at onceSmaller images, recyclingKey / recycling lists, placeholders
GIFs and huge PNGsWebP / AVIF, video for long animations
Layout jumps when images loadFixed dimensions or aspectRatio with blurhash or thumbhash placeholders

6. Startup Time

TechniqueEffect
Hermes bytecode (default)No JS parse at startup
Lazy imports / inline requiresOnly evaluate modules when first used (Metro supports inlineRequires)
Avoid top-level side effectsDo not initialize SDKs, read storage, or build large objects at import time
Defer non-critical SDKsInit analytics and crash reporting after first frame (crash reporting as early as possible)
Lazy screens and tabsLoad code for screens the user has not opened yet on demand
Cache-first renderingShow persisted data (MMKV, SQLite, persisted query cache) immediately
Keep the splash screen until readyAvoid a blank flash, but do not hide slow startup behind a long splash
Native sideAvoid heavy work in Application.onCreate / AppDelegate; Android Baseline Profiles help native startup
Precompiled iOS buildsRN ships precompiled iOS dependencies, which speeds up build time (not runtime)

Barrel files (index.ts re-exporting everything) can force the evaluation of whole libraries at startup; import from specific paths or rely on tree-shaking support in Metro and Expo.


7. Bundle and App Size

  • Analyze the JS bundle with react-native-bundle-visualizer or Expo Atlas (EXPO_ATLAS=1 npx expo start).
  • Replace heavy libraries: moment with date-fns or Intl, full lodash with per-method imports, large icon fonts with only the icons you use.
  • Android: ship App Bundles (AAB) so Play delivers per-ABI and per-density splits; enable R8 minification and resource shrinking.
  • iOS: App Thinning delivers per-device assets; strip unused architectures and debug symbols from the binary (upload dSYMs separately).
  • Remote assets and fonts you rarely use instead of bundling everything.

8. Memory Leaks

LeakFix
Listeners not removed (AppState, Keyboard, Dimensions, native emitters)Return a cleanup that calls .remove()
Timers and intervals left runningClear in cleanup, pause on blur
Stale closures holding large dataAvoid capturing big objects in long-lived callbacks
Unbounded caches (images, query cache, in-memory arrays)Limits and gcTime; trim on memory warnings
Screens never popped (push loops)Reset stacks after long flows
Native views (maps, video, camera) not releasedUnmount or pause them when hidden

Tools: Hermes heap snapshots and allocation timelines in React Native DevTools (Memory panel), Xcode Instruments (Leaks, Allocations), Android Studio Memory Profiler and LeakCanary for native leaks.


9. Android-Specific Performance

Android is usually the harder platform because of device diversity.

  • Overdraw: enable "Debug GPU overdraw" in developer options; remove redundant backgrounds.
  • Shadows and elevation are costly when stacked; use them sparingly in lists.
  • Text rendering and custom fonts are slower; avoid re-measuring large text blocks.
  • ANRs (App Not Responding) happen when the main thread is blocked for about 5 seconds; often caused by heavy native SDK work on startup or sync disk I/O.
  • Test on low-RAM devices where the OS kills background apps and the app process often restarts.
  • Use Android vitals in Play Console for real-world ANR, crash, and slow-frame rates.

10. Debugging Tools

ToolUse
React Native DevTools (press j in Metro)Chrome DevTools-based debugger for Hermes: breakpoints, console, React Components and Profiler, memory, network and performance panels in recent versions
Dev menu (shake, Cmd+D / Cmd+M)Reload, perf monitor overlay, element inspector
LogBoxIn-app warnings and errors with stack traces
Expo dev tools pluginsInspect React Query, storage, navigation state, network in the dev client
ReactotronInspect state, API calls, and logs
Xcode (Instruments, Memory Graph, View Debugger)iOS native performance, leaks, native crashes
Android Studio (Logcat, Profiler, Layout Inspector)Android native logs, CPU, memory
Perfetto / systraceSystem-wide traces across JS, UI, and render threads on Android
FlipperRemoved from the RN template (0.74); replaced by React Native DevTools and Expo tools
Remote JS debugging in ChromeRemoved; it ran JS in Chrome's V8 instead of on the device and hid real performance

Debugging native crashes: read adb logcat on Android and the Xcode console or crash logs on iOS; JS stack traces will not show native exceptions.


11. Crashes and Error Monitoring

// Catch render errors per screen so one broken widget does not kill the app.
<ErrorBoundary fallback={<ErrorScreen onRetry={reset} />}>
  <Feed />
</ErrorBoundary>
  • JS errors: error boundaries for render errors, a global handler for unhandled exceptions and promise rejections.
  • Native crashes: need a native SDK (Sentry, Crashlytics, Bugsnag) that captures signals and exceptions on both platforms.
  • Source maps: upload the Hermes source maps for each release (and dSYMs / ProGuard mappings) or stack traces are unreadable.
  • Tag reports with the app version and OTA update ID, since one binary can run several JS bundles.
  • Track ANRs, app hangs, and slow and frozen frames, not only crashes.

12. Performance Checklist

  1. Profile release builds on a low-end Android device.
  2. Identify the slow thread before changing code.
  3. Virtualize long lists; memoize rows; prefer FlashList for heavy feeds.
  4. Enable the React Compiler; split contexts; select narrowly from stores.
  5. Run animations and gestures on the UI thread (Reanimated, Gesture Handler).
  6. Size, cache, and compress images.
  7. Lazy-load modules and screens; avoid top-level side effects.
  8. Cache data locally for instant startup.
  9. Remove console.log and dev-only code from release builds.
  10. Monitor crashes, ANRs, startup, and slow frames in production, and alert on regressions.

13. Questions

Q: The app is slow. Walk me through how you would fix it. Reproduce in a release build on a low-end device, define the metric (startup, scroll FPS, transition time), use the perf monitor to see whether JS or UI FPS drops, profile that thread (React DevTools Profiler and Hermes profiles for JS, Instruments / Android profiler / Perfetto for native), fix the top offender, measure again, and add a regression check.

Q: How do you prevent unnecessary re-renders? Keep state close to where it is used, split contexts, subscribe to minimal store slices, keep props referentially stable (React Compiler or memo hooks), memoize expensive children, and keep per-frame values out of React state.

Q: How do you improve cold start time? Hermes bytecode, lazy imports and inline requires, no work at module top level, deferred SDK initialization, lazy screens, cache-first rendering, and reducing native startup work.

Q: Why should you not judge performance in development mode? Dev builds run extra checks, warnings, and unoptimized code, and include debugging overhead, so they are much slower and do not reflect what users see.

Q: What replaced Flipper and Chrome remote debugging? React Native DevTools, which connects directly to Hermes on the device through the Chrome DevTools Protocol, so debugging reflects real runtime behavior.

Q: How do you debug a crash that only happens in production? Use crash reporting with uploaded source maps and native symbols, check the app version and OTA update ID, read breadcrumbs, reproduce with a release build of the same bundle, and check native logs for native exceptions.

On this page