Understand React Native’s multi-threaded architecture and how threads coordinate
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.
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); });}
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
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 inputfunction handleTextInput(text) { setText(text); // Scheduled as UserBlockingPriority}// Normal priority - data fetch resultfetch('/api/data').then(data => { setData(data); // Scheduled as NormalPriority});// Low priority - analyticslogAnalyticsEvent('screen_view'); // Scheduled as LowPriority
From ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md:The Animation Backend allows animation frameworks to update props without going through React:
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,};
// Bad - JSI value escaped to background threadvoid 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 sharevoid 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();}
// Shadow nodes are immutable - safe to share!auto shadowNode = std::make_shared<const ShadowNode>(props, children);// Can safely read from multiple threadsstd::thread([shadowNode]() { auto props = shadowNode->getProps(); // Read-only access is safe}).detach();
// Bad - many thread switchesfor (int i = 0; i < 100; i++) { runtimeExecutor_([i](jsi::Runtime& runtime) { processItem(runtime, i); });}// Good - single thread switchruntimeExecutor_([](jsi::Runtime& runtime) { for (int i = 0; i < 100; i++) { processItem(runtime, i); }});
// Bad - multiple rendersitems.forEach(item => { processItem(item); // Each triggers render});// Good - single renderconst results = items.map(processItem);setResults(results); // Single render
// Bad - blocks JavaScript threadfunction processLargeDataset(data) { return data.map(heavyComputation); // Blocks!}// Good - offload to native or workerasync function processLargeDataset(data) { return await NativeModule.processInBackground(data);}