Before writing any code, it’s crucial to understand how React Native works under the hood. This knowledge will help you debug issues, optimize performance, and make better architectural decisions throughout your React Native journey.What You’ll Learn:
What React Native is and how it differs from other solutions
React Native is an open-source framework created by Meta (Facebook) in 2015 that allows developers to build truly native mobile applications using JavaScript and React. Unlike hybrid frameworks that use WebViews, React Native renders actual native UI components.Think of it like a universal translator: you write your instructions in JavaScript, and React Native translates them into the native language each platform speaks — Objective-C/Swift for iOS and Java/Kotlin for Android. The result is not a translation of a web page crammed into a phone, but a genuinely native app that uses the same UI building blocks as apps written in Xcode or Android Studio.
True Native
Renders to real native components (UIView, TextView), not web views
Cross-Platform
One codebase for iOS and Android with 90%+ code reuse
Hot Reloading
See changes instantly without rebuilding the entire app
JavaScript
Use the language and ecosystem you already know
Large Ecosystem
Access to npm packages and React libraries
Active Community
Backed by Meta with millions of developers worldwide
Think of the bridge like sending letters between two people who speak different languages. Each message has to be written down (serialized to JSON), put in an envelope, delivered, translated, and then acted upon. This works, but it is slow when you need rapid back-and-forth communication — like coordinating a 60 FPS animation where every frame is a new letter.
JavaScript Thread: Your React code runs here, handling business logic, state management, and UI declarations
Bridge: Serializes messages to JSON and passes them asynchronously between threads
Native Thread: Receives commands and renders actual native UI components
Shadow Thread: Calculates layout using Yoga (Flexbox engine)
The legacy bridge has several limitations that can impact performance:
Asynchronous Only: All communication is async, causing delays for time-sensitive operations
Serialization Overhead: Data must be serialized to JSON, adding CPU overhead
Single Bottleneck: All communication goes through one bridge
No Direct Access: JavaScript can’t directly call native methods
// Example: Bridge latency issue with animations// This animation might feel janky because each frame// requires a round-trip through the bridge (JS -> JSON -> Native -> render)const opacity = useRef(new Animated.Value(0)).current;Animated.timing(opacity, { toValue: 1, duration: 300, // useNativeDriver: true sends the entire animation description to the // native side once, so all 60 frames/sec are calculated on the UI thread // without crossing the bridge. This is critical for smooth animations. // Without it, each frame would require a JS-to-native round trip. useNativeDriver: true,}).start();
JSI is the foundation of the new architecture — a lightweight C++ layer that allows JavaScript to hold references to native objects and call methods directly. If the old bridge was like sending letters, JSI is like the two people learning to speak the same language (C++) so they can have a real-time conversation with no translation delays:
// Conceptual example of JSI benefits// Old Bridge: Async, serializedbridge.call('NativeModule', 'getData', { id: 123 }) .then(data => console.log(data));// New JSI: Direct, can be syncconst data = NativeModule.getData({ id: 123 }); // Direct call!
JSI Benefits:
No Serialization: Objects are passed by reference, not serialized to JSON
Synchronous Calls: When needed, JS can call native code synchronously
Lazy Loading: Native modules load only when first accessed
Multiple Engines: JSI abstracts the JS engine, enabling Hermes, JSC, or V8
Benchmarks from Meta’s testing on a mid-range Android deviceThe startup improvement is especially significant on Android, where device specs vary wildly. On a budget Android phone with 2-3 GB of RAM, the memory savings from Hermes can be the difference between your app running smoothly and the OS killing it in the background. On iOS, the improvements are present but less dramatic since Apple devices tend to have more consistent hardware specs.
✅ Your team knows JavaScript/TypeScript and React✅ You need to ship iOS and Android apps quickly✅ You want near-native performance with code reuse✅ You need access to the npm ecosystem✅ You’re building a startup MVP or enterprise app✅ You want to share code with a React web app✅ You need over-the-air updates (CodePush/EAS Update)
✅ You need absolute best performance (games, AR/VR)✅ You’re building complex animations/graphics✅ Your app requires cutting-edge platform features immediately✅ You have separate iOS and Android teams✅ You’re building platform-specific experiences✅ App size is critical (React Native adds ~10MB)
✅ You want pixel-perfect custom UI everywhere✅ Your team doesn’t have JavaScript experience✅ You need excellent animation performance out of the box✅ You’re building from scratch (no existing React code)✅ You prefer strongly-typed languages (Dart)✅ You want a single rendering engine across platforms
┌─────────────────────────────────────────────────────────────────────────────┐│ Technology Decision Framework │├─────────────────────────────────────────────────────────────────────────────┤│ ││ Question 1: Does your team know React/JavaScript? ││ ├── Yes ──► Strong candidate for React Native ││ └── No ──► Consider Flutter or Native ││ ││ Question 2: Do you need to share code with web? ││ ├── Yes ──► React Native + React Native Web ││ └── No ──► Any option works ││ ││ Question 3: Is performance absolutely critical? ││ ├── Yes (games, AR) ──► Native or Flutter ││ └── No (most apps) ──► React Native is fine ││ ││ Question 4: Do you need OTA updates? ││ ├── Yes ──► React Native (CodePush/EAS Update) ││ └── No ──► Any option works ││ ││ Question 5: Timeline and budget? ││ ├── Fast & Limited ──► React Native or Flutter ││ └── Flexible ──► Any option works ││ │└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐│ React Native Threads │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 1. JavaScript Thread ││ ───────────────── ││ • Runs your React code ││ • Handles business logic ││ • Manages state and props ││ • Single-threaded (one JS thread) ││ ││ 2. Main/UI Thread ││ ───────────────── ││ • Renders native UI ││ • Handles touch events ││ • Must stay responsive (60 FPS = 16ms per frame) ││ • Platform's main thread ││ ││ 3. Shadow Thread (Legacy) / C++ Thread (New Arch) ││ ───────────────────────────────────────────────── ││ • Calculates layout using Yoga ││ • Determines component positions and sizes ││ • Runs Flexbox calculations ││ ││ 4. Native Modules Thread ││ ───────────────────── ││ • Runs native module code ││ • Handles async operations ││ • Camera, GPS, file system, etc. ││ ││ Performance Tip: ││ Keep the JS thread free for UI updates. Heavy computations ││ should be moved to native modules or web workers. ││ ││ Practical Pitfall: ││ If your JS thread is busy (e.g., parsing large JSON), touch ││ events will still be detected by the native UI thread, but the ││ JS callback won't fire until the JS thread is free. This is why ││ buttons can feel "unresponsive" during heavy computation. ││ │└─────────────────────────────────────────────────────────────────────────────┘
// This runs on JS threadconst handlePress = () => { // This computation blocks the JS thread const result = heavyComputation(); // ❌ Bad - blocks UI updates // Better: Use native module or InteractionManager InteractionManager.runAfterInteractions(() => { const result = heavyComputation(); // ✅ Runs after animations complete });};// Even better: Use useNativeDriver for animationsAnimated.timing(opacity, { toValue: 1, duration: 300, useNativeDriver: true, // ✅ Animation runs on UI thread, not JS thread}).start();
Q1: What makes React Native different from hybrid frameworks like Ionic?
Answer: React Native renders to actual native UI components (UIView, TextView), while hybrid frameworks render to WebViews. This gives React Native near-native performance and platform-specific look and feel.
Q2: What are the three main components of the new architecture?
Answer:
JSI (JavaScript Interface): Direct C++ bindings between JS and native
Fabric: New rendering system with synchronous layout
Answer: Hermes compiles JavaScript to bytecode at build time (ahead-of-time compilation), while JSC compiles at runtime. This means Hermes apps start faster because they skip parsing and compilation at launch.
Q4: When should you NOT choose React Native?
Answer: Avoid React Native for:
Performance-critical apps (games, AR/VR)
Apps requiring immediate access to cutting-edge platform features
Walk me through what happens from the moment a user taps a button in a React Native app to when the native UI updates. Cover both the old bridge and new JSI architecture.
Strong Answer:
Old bridge path: The user’s tap is detected by the native UI thread, which serializes the touch event into a JSON message and sends it across the bridge to the JS thread. The JS thread processes the event (running your onPress handler), updates React state, triggers reconciliation, computes the minimal set of UI changes, serializes those changes to JSON, and sends them back across the bridge to the native thread, which applies the updates and re-renders the affected native views. Total: two bridge crossings, two JSON serialization/deserialization cycles.
New architecture (JSI) path: The tap event can be dispatched directly to the JS thread via JSI’s C++ bindings — no JSON serialization. The JS handler runs, React reconciles, and the resulting UI mutations are applied directly to the shared C++ shadow tree that both JS and native threads can access. The native renderer picks up changes from the shadow tree and commits them to the screen. Total: zero serialization, potentially synchronous for layout measurements.
The practical difference becomes visible in gesture-heavy interactions: a drag-to-reorder list on the old bridge feels subtly laggy because each drag position update requires a full bridge round-trip. On JSI with Reanimated, the gesture handler runs entirely on the UI thread via worklets, only notifying JS when the gesture completes. The result is 60fps drag interactions even on mid-range devices.
Follow-up: How does useNativeDriver: true on the Animated API bypass the bridge, and why can it only animate certain properties?Follow-up Answer:
When you set useNativeDriver: true, the Animated API serializes the entire animation description (start value, end value, duration, easing function) into a single message and sends it to the native side once at the start. The native animation driver then runs all frames on the UI thread without any further JS involvement. This is why it can maintain 60fps even when the JS thread is busy.
The limitation is that the native animation driver can only operate on properties that exist in the native view’s layout system and can be updated without triggering a full layout recalculation. Transform properties (translateX, translateY, scale, rotate) and opacity work because they are composited by the GPU without affecting surrounding views. Properties like width, height, margin, and backgroundColor do not work with useNativeDriver because changing them requires Yoga to recalculate layout for the entire subtree, which must happen on the JS/shadow thread.
This is exactly why Reanimated exists — it provides its own worklet-based animation system that runs on the UI thread and can animate any property, including layout properties, by performing layout calculations natively.
Hermes compiles JavaScript to bytecode at build time. What are the specific trade-offs compared to a JIT compiler like V8, and when might Hermes be slower?
Strong Answer:
Hermes uses Ahead-of-Time (AOT) compilation: JavaScript is parsed, compiled to bytecode, and optimized at build time. At runtime, the Hermes VM interprets this bytecode directly. V8 (and JavaScriptCore to a lesser extent) use Just-in-Time (JIT) compilation: they start interpreting, identify hot code paths at runtime, and compile those paths to optimized machine code on the fly.
Hermes wins on startup time (no parse/compile phase at launch), memory usage (bytecode is more compact than source + AST, and no JIT compiler running in memory), and app size (bytecode is smaller than minified JavaScript). These are exactly the metrics that matter most on mobile.
Hermes can be slower for long-running CPU-intensive computations. A JIT compiler like V8 can optimize hot loops into near-native-speed machine code, while Hermes’s interpreter runs bytecode with a fixed overhead per instruction. In practice, this matters for: heavy data processing (sorting 100K records client-side), cryptographic operations in JS, and complex regex matching on large strings.
The pragmatic response: these scenarios should not be happening in JS on mobile anyway. Heavy computation should be offloaded to native modules or performed server-side. For the 99% of mobile app code that is UI rendering, event handling, and API calls, Hermes’s startup and memory advantages far outweigh the JIT speed advantage on hot loops.
Follow-up: You notice your Hermes-powered app has a 3-second cold start time on a mid-range Android device. Walk me through how you would diagnose and reduce this.Follow-up Answer:
First, measure what is actually happening during those 3 seconds. Use performance.now() markers at key points: native app launch, JS bundle load start/end, first React render, and time-to-interactive. Hermes provides runtime properties via global.HermesInternal.getRuntimeProperties() that tell you bytecode load time and heap size.
Common culprits: too many native modules loading eagerly at startup (migrate to TurboModules for lazy loading), large JS bundle size (enable Metro’s tree shaking, audit your imports for accidentally bundling entire libraries), expensive top-level code that runs during module evaluation (move initialization into lazy useEffect calls), and synchronous storage reads at startup blocking the render.
Practical fixes in priority order: (1) enable Hermes if not already enabled, (2) use require calls inside functions instead of top-level imports for non-critical modules, (3) defer non-essential initialization to after the first frame using InteractionManager.runAfterInteractions(), (4) show a native splash screen during JS initialization so the user sees something immediately, (5) audit and reduce the number of providers wrapping your app root — each context provider is a React component that renders during the critical path.
When would you recommend against using React Native for a project? Give me specific scenarios, not generic hand-waving.
Strong Answer:
Scenario 1: A game studio building a 3D mobile game with physics simulation. React Native’s rendering pipeline is designed for form-based UI components, not GPU-intensive rendering. The JS-to-native abstraction adds overhead that is unacceptable when you need to render 10,000 particles at 60fps. Use Unity, Unreal, or native Metal/Vulkan.
Scenario 2: A company with established, mature iOS and Android teams who already have a shared C++ core library. Adding React Native introduces a third language (JavaScript), a third build system (Metro), and a third paradigm (React) without retiring the existing ones. The coordination cost outweighs the code-sharing benefit. Better to invest in KMM (Kotlin Multiplatform Mobile) for shared business logic while keeping native UI.
Scenario 3: An app that needs same-day access to a brand-new platform API (like a new iOS widget type released at WWDC). React Native and its community libraries lag behind native SDKs by weeks to months. If your competitive advantage depends on being first to adopt new platform features, native gives you that capability.
Scenario 4: A hardware companion app that requires constant Bluetooth Low Energy communication with a custom protocol. BLE in React Native requires native modules, and the async bridge (even with JSI) introduces latency that can break timing-sensitive BLE protocols. The entire core functionality would end up in native code, making React Native just an expensive wrapper around native screens.
The pattern: React Native excels at content-driven, form-heavy, CRUD-style apps (which is 80% of all apps). It struggles when the core value proposition requires sustained, low-latency access to hardware or GPU.
Follow-up: Your team decided to use React Native, but 6 months in you discover that a critical feature requires native-level performance. What is your recovery plan?Follow-up Answer:
Do not panic and do not rewrite. React Native’s architecture explicitly supports this scenario through native modules. Identify the specific feature that needs native performance, define a clear API boundary (what data goes in, what comes out), and implement it as a TurboModule.
For example, if the feature is real-time video processing, the native module handles camera capture and frame processing in Swift/Kotlin, exposes a processFrame() function via JSI, and returns results to the JS layer. The other 50 screens in your app remain React Native and benefit from the cross-platform code sharing.
The key architectural decision is the API boundary. Design it as a thin, stable interface that decouples the native implementation from the JS consumer. Use TypeScript specs with Codegen so the contract is enforced at compile time. This way, the native team can optimize the module’s internals without breaking the React Native screens that consume it.