Skip to main content
React Native uses a sophisticated multi-threaded architecture to achieve optimal performance. Understanding this threading model is crucial for building performant applications and debugging issues.

Thread Overview

React Native applications typically use four main threads:

1. JavaScript Thread

Executes all your JavaScript code:
  • React rendering - Component renders and reconciliation
  • State updates - Processing setState and hooks
  • Event handlers - Button presses, input changes, etc.
  • Business logic - App-specific computations
  • Redux/state management - State transitions
  • Async operations - Promise chains, async/await
In the new architecture, this is where JSI enables synchronous calls to native code without thread switching.

2. UI Thread (Main Thread)

The native platform’s main thread:
  • View rendering - Drawing pixels to screen
  • Layout application - Positioning views
  • User input - Touch events, keyboard input
  • Animations - Native animation execution
  • View mutations - Creating, updating, destroying views
  • Platform APIs - iOS UIKit, Android View system calls
Blocking the UI thread causes jank and unresponsive UI. Keep operations under 16ms for 60 FPS.

3. Shadow Thread (Background Thread)

Handles layout computation in Fabric:
  • Yoga layout - Flexbox calculations
  • Shadow tree operations - Cloning, diffing shadow nodes
  • Commit phase - Processing React commits
  • Diffing - Generating mutation lists
Benefits:
  • UI thread stays responsive during layout
  • Complex layouts don’t block user interaction
  • Can compute layout for upcoming screens

4. Native Modules Background Threads

TurboModules can specify thread requirements:
  • Network requests - HTTP calls, downloads
  • Database operations - SQLite queries
  • File I/O - Reading/writing files
  • Image processing - Decoding, resizing
  • Heavy computations - Cryptography, compression

Threading in the New Architecture

JSI and Synchronous Calls

With JSI, JavaScript can call native code synchronously:
// JavaScript thread
const result = NativeModule.getValue(); // Synchronous!
console.log(result); // Available immediately
C++ implementation:
jsi::Value getValue(jsi::Runtime& runtime, const jsi::Value* args, size_t count) {
  // Executes on JavaScript thread
  return jsi::String::createFromUtf8(runtime, "value");
}
This executes on the JavaScript thread - no thread switch!

RuntimeExecutor

The RuntimeExecutor safely schedules work on the JavaScript thread from any thread. Defined in ReactCommon/runtimeexecutor/ReactCommon/RuntimeExecutor.h:
using RuntimeExecutor = std::function<void(
  std::function<void(jsi::Runtime& runtime)> &&callback
)>;
Usage pattern:
void BackgroundWork::processData(const Data& data) {
  // Running on background thread
  
  auto result = computeExpensiveResult(data);
  
  // Schedule callback on JavaScript thread
  runtimeExecutor_([result](jsi::Runtime& runtime) {
    // Now on JavaScript thread, safe to use JSI
    auto jsResult = jsi::Object(runtime);
    jsResult.setProperty(runtime, "value", jsi::Value(result));
    
    // Call JavaScript callback
    auto callback = getCallback(runtime);
    callback.call(runtime, jsResult);
  });
}
Key benefits:
  • Thread safety - Runtime access serialized
  • Non-blocking - Posts to queue, doesn’t wait
  • Error handling - Exceptions caught and reported

CallInvoker

TurboModules use CallInvoker for thread management:
class CallInvoker {
public:
  // Schedule work asynchronously
  virtual void invokeAsync(std::function<void()> &&func) = 0;
  
  // Execute work synchronously (blocks caller)
  virtual void invokeSync(std::function<void()> &&func) = 0;
};
Example - calling JavaScript callback from native:
void MyTurboModule::processData(jsi::Runtime& runtime, 
                                jsi::Function callback,
                                std::string data) {
  // Do work on background thread
  std::thread([this, callback = std::move(callback), data]() {
    auto result = process(data);
    
    // Call JavaScript callback on JS thread
    jsInvoker_->invokeAsync([callback, result](jsi::Runtime& runtime) {
      callback.call(runtime, jsi::String::createFromUtf8(runtime, result));
    });
  }).detach();
}

Event Loop Integration

The JavaScript thread implements an event loop aligned with web standards. From ReactCommon/react/renderer/runtimescheduler/__docs__/README.md:

Event Loop Phases

Each tick of the event loop:
  1. Select Task - Choose next task by priority
  2. Execute Task - Run the task’s JavaScript/C++ callback
  3. Process Microtasks - Drain microtask queue (Promises, queueMicrotask)
  4. Update Rendering - Flush UI updates to host platform
┌─────────────────────┐
│  1. Select Task     │
│  (by priority)      │
└───────┬──────────────┘


┌───────┴──────────────┐
│  2. Execute Task    │
│  (JS or C++)        │
└───────┬──────────────┘


┌───────┴──────────────┐
│  3. Microtasks      │
│  (Promises, etc.)   │
└───────┬──────────────┘


┌───────┴──────────────┐
│  4. Update Render   │
│  (Flush to UI)      │
└─────────────────────┘

Atomicity of UI Updates

Each event loop tick produces one atomic UI update:
function handlePress() {
  // Task: Handle press event
  setState1(value1);
  setState2(value2);
  setState3(value3);
  
  // Microtask: React processes state updates
  // All three updates processed together
  
  // Update rendering: All changes flushed to UI at once
  // User never sees intermediate states!
}
Benefits:
  • Predictable - UI updates happen at well-defined points
  • Atomic - No partial updates visible to user
  • Aligned with web - Same semantics as browsers

Task Priorities

RuntimeScheduler supports priority-based scheduling:
enum class SchedulerPriority {
  ImmediatePriority = 1,      // Sync, time-critical
  UserBlockingPriority = 2,   // User input, interactions
  NormalPriority = 3,         // Default for most work
  LowPriority = 4,            // Analytics, logging
  IdlePriority = 5,           // Background work
};
Higher priority tasks run first:
// High priority - user input
function handleTextInput(text) {
  setText(text); // Scheduled as UserBlockingPriority
}

// Normal priority - data fetch result
fetch('/api/data').then(data => {
  setData(data); // Scheduled as NormalPriority
});

// Low priority - analytics
logAnalyticsEvent('screen_view'); // Scheduled as LowPriority

Fabric Rendering Pipeline Threading

Phase 1: Render (JavaScript Thread)

// React component renders
function MyComponent({data}) {
  return <View><Text>{data}</Text></View>;
}
Operations:
  • React reconciliation
  • Call Fabric UIManager via JSI
  • Create/clone shadow nodes
  • Build new shadow tree
Thread: JavaScript thread

Phase 2: Commit (Background Thread)

void ShadowTree::commit(ShadowTreeCommitOptions options) {
  // Commit hooks (pre-layout)
  notifyPreCommit();
  
  // Layout calculation
  YGNodeCalculateLayout(rootYogaNode, ...);
  
  // Commit hooks (post-layout)
  notifyPostCommit();
  
  // Generate diff
  auto mutations = calculateMutations(oldTree, newTree);
  
  // Create mounting transaction
  auto transaction = MountingTransaction(mutations);
}
Operations:
  • Run commit hooks
  • Calculate layout (Yoga)
  • Diff shadow trees
  • Generate mutation list
Thread: Shadow thread (background)

Phase 3: Mount (UI Thread)

// Android MountingManager
public void executeMount(MountingTransaction transaction) {
  for (MountItem item : transaction.getMountItems()) {
    switch (item.getType()) {
      case CREATE:
        createView(item);
        break;
      case UPDATE:
        updateView(item);
        break;
      case DELETE:
        deleteView(item);
        break;
    }
  }
}
Operations:
  • Apply mutations to views
  • Create/update/delete native views
  • Set view properties
  • Update layout
Thread: UI thread (main thread)

Thread Coordination

JavaScript Thread          Shadow Thread           UI Thread
      |                          |                      |
      | React renders             |                      |
      |------------------------->|                      |
      |                          | Layout calculation   |
      |                          | (Yoga)               |
      |                          |                      |
      | Continue JS execution    | Generate diff        |
      |                          |                      |
      |                          |--------------------->|
      |                          |                      | Apply mutations
      |                          |                      | Update views
      |                          |                      |
      | Fire onLayout callbacks  |                      |
      |<---------------------------------------------||
Key point: JavaScript thread doesn’t block waiting for layout or mounting!

TurboModule Threading

Synchronous Methods

Execute on JavaScript thread:
// Spec
interface Spec extends TurboModule {
  getValue(): string; // Sync
}
// Implementation
- (NSString *)getValue {
  // Runs on JavaScript thread!
  // Must be fast (< 1ms)
  return self.cachedValue;
}
Synchronous TurboModule methods block the JavaScript thread. Keep them fast or users will see frozen UI.

Asynchronous Methods

Can run on any thread:
// Spec  
interface Spec extends TurboModule {
  fetchData(url: string): Promise<Data>; // Async
}
// Implementation
- (void)fetchData:(NSString *)url
          resolve:(RCTPromiseResolveBlock)resolve
           reject:(RCTPromiseRejectBlock)reject {
  // Dispatch to background queue
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSData *data = [self performNetworkRequest:url];
    
    // Resolve on JavaScript thread (automatic)
    resolve(data);
  });
}
The promise resolution automatically happens on the JavaScript thread.

Specifying Thread Requirements

iOS - require main thread:
+ (BOOL)requiresMainQueueSetup {
  return YES;
}

- (void)updateUI {
  dispatch_assert_queue(dispatch_get_main_queue());
  // Safe to update UI
}
Android - require UI thread:
@ReactMethod
fun updateUI() {
  UiThreadUtil.runOnUiThread {
    // Safe to update UI
  }
}

Animation Threading

JavaScript-Driven Animations

// Runs on JavaScript thread
Animated.timing(animatedValue, {
  toValue: 100,
  duration: 300,
  useNativeDriver: false, // JS thread
}).start();
Threading:
  • Animation ticks on JavaScript thread
  • Each tick triggers Fabric render
  • Can drop frames if JS thread busy

Native-Driven Animations

Animated.timing(animatedValue, {
  toValue: 100,
  duration: 300,
  useNativeDriver: true, // UI thread!
}).start();
Threading:
  • Animation runs on UI thread
  • Bypasses JavaScript thread entirely
  • Smooth even if JavaScript thread blocked

Animation Backend

From ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md: The Animation Backend allows animation frameworks to update props without going through React:
Animation Framework
       |
       v
Animation Backend
       |
       +---> [Layout props] ---> Fabric Commit (Shadow Thread)
       |
       +---> [Non-layout props] ---> SynchronouslyUpdateProps (UI Thread)
Threading benefits:
  • Layout animations on shadow thread
  • Non-layout animations on UI thread
  • JavaScript thread not involved

Event Handling Threading

Event Emission

Native to JavaScript:
// iOS - UI thread
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  // Running on UI thread
  
  RCTTouchEvent *touchEvent = [[RCTTouchEvent alloc] initWithEventName:@"onTouchStart"
                                                                touches:touches
                                                             forView:self];
  
  // Event emitted to JavaScript thread
  [self.eventEmitter dispatchEvent:touchEvent];
}

Event Priorities

From ReactCommon/react/renderer/core/EventEmitter.h:
enum class EventPriority {
  // Processed immediately on next frame
  Discrete,
  
  // Batched and throttled
  Continuous,
  
  // Batched with event loop
  AsynchronousBatched,
};
Examples:
  • Discrete: Press, focus, blur
  • Continuous: Scroll, drag, mouse move
  • AsynchronousBatched: Most custom events

Event Beat

Synchronizes events with rendering:
class EventBeat {
public:
  // Request beat (from any thread)
  virtual void request() const = 0;
  
  // Beat triggered on JavaScript thread
  void beat() const;
};
Ensures events are batched and processed with rendering updates.

Thread Safety Best Practices

1. Never Share JSI Values Across Threads

// Bad - JSI value escaped to background thread
void badExample(jsi::Runtime& runtime) {
  auto value = runtime.global().getProperty(runtime, "someValue");
  
  std::thread([value]() { // DON'T DO THIS!
    // Crash! value used on wrong thread
  }).detach();
}

// Good - extract data first, then share
void goodExample(jsi::Runtime& runtime) {
  auto value = runtime.global().getProperty(runtime, "someValue");
  auto data = value.getString(runtime).utf8(runtime);
  
  std::thread([data]() {
    // OK - data is std::string, not JSI value
    process(data);
  }).detach();
}

2. Use RuntimeExecutor for JavaScript Thread Access

class MyModule {
public:
  void backgroundWork() {
    std::thread([this]() {
      auto result = performWork();
      
      // Schedule callback on JavaScript thread
      runtimeExecutor_([result](jsi::Runtime& runtime) {
        notifyJavaScript(runtime, result);
      });
    }).detach();
  }
  
private:
  RuntimeExecutor runtimeExecutor_;
};

3. Protect Shared State with Mutexes

class ThreadSafeCache {
public:
  void set(const std::string& key, const std::string& value) {
    std::lock_guard<std::mutex> lock(mutex_);
    cache_[key] = value;
  }
  
  std::optional<std::string> get(const std::string& key) {
    std::lock_guard<std::mutex> lock(mutex_);
    auto it = cache_.find(key);
    if (it != cache_.end()) {
      return it->second;
    }
    return std::nullopt;
  }
  
private:
  std::mutex mutex_;
  std::unordered_map<std::string, std::string> cache_;
};

4. Prefer Immutable Data Structures

// Shadow nodes are immutable - safe to share!
auto shadowNode = std::make_shared<const ShadowNode>(props, children);

// Can safely read from multiple threads
std::thread([shadowNode]() {
  auto props = shadowNode->getProps();
  // Read-only access is safe
}).detach();

5. Use Weak Pointers to Avoid Cycles

class Observer {
public:
  void observe(std::shared_ptr<Subject> subject) {
    subject_ = subject; // Store weak_ptr
    
    subject->addCallback([weak = std::weak_ptr<Observer>(shared_from_this())]() {
      if (auto self = weak.lock()) {
        self->handleUpdate();
      }
    });
  }
  
private:
  std::weak_ptr<Subject> subject_;
};

Debugging Threading Issues

Detect Thread Violations

iOS:
// Enable thread assertions
RCTAssertMainQueue();
RCTAssertNotMainQueue();
Android:
// Check thread
if (!UiThreadUtil.isOnUiThread()) {
  throw IllegalStateException("Must be called on UI thread")
}

Thread Sanitizer

Enable ThreadSanitizer to detect races: Xcode:
  1. Edit Scheme
  2. Run tab
  3. Diagnostics
  4. Enable “Thread Sanitizer”
Android:
android {
  defaultConfig {
    externalNativeBuild {
      cmake {
        arguments "-DSANITIZE_THREAD=ON"
      }
    }
  }
}

Logging Thread IDs

void logCurrentThread(const std::string& location) {
  std::stringstream ss;
  ss << location << " on thread " << std::this_thread::get_id();
  std::cout << ss.str() << std::endl;
}

void myFunction(jsi::Runtime& runtime) {
  logCurrentThread("myFunction");
  // ...
}

Xcode Instruments

  • Time Profiler - See which threads are busy
  • System Trace - Visualize thread activity
  • Thread States - Identify blocked threads

Performance Optimization

Minimize Thread Switching

// Bad - many thread switches
for (int i = 0; i < 100; i++) {
  runtimeExecutor_([i](jsi::Runtime& runtime) {
    processItem(runtime, i);
  });
}

// Good - single thread switch
runtimeExecutor_([](jsi::Runtime& runtime) {
  for (int i = 0; i < 100; i++) {
    processItem(runtime, i);
  }
});

Batch UI Updates

// Bad - multiple renders
items.forEach(item => {
  processItem(item); // Each triggers render
});

// Good - single render
const results = items.map(processItem);
setResults(results); // Single render

Offload Heavy Work

// Bad - blocks JavaScript thread
function processLargeDataset(data) {
  return data.map(heavyComputation); // Blocks!
}

// Good - offload to native or worker
async function processLargeDataset(data) {
  return await NativeModule.processInBackground(data);
}

Use requestIdleCallback

// Low-priority work
requestIdleCallback(() => {
  performAnalytics();
  cleanupCache();
  prefetchData();
});
Runs when JavaScript thread is idle, won’t block user interactions.

Further Reading

Architecture Overview

See how threading fits in the architecture

JavaScript Interface

JSI and thread safety

Fabric Renderer

Fabric’s multi-threaded rendering

TurboModules

Threading in TurboModules

Build docs developers (and LLMs) love