margelo/react-native-runtimes

Run heavy React Native components and business logic in isolated Hermes runtimes (without freezing your main JS thread)

TypeScript

562

96 commits

updated Aug 5, 2026

See the code

README

react-native-runtimes logo

react-native-runtimes

Run heavy React Native UI and business logic in isolated Hermes runtimes β€” without freezing your main JS thread

React Native New Architecture Expo Android iOS License: MIT

πŸ“– Docs Β· ⚑ Quick Start Β· πŸ“¦ Core Β· πŸ—‚ State


Margelo Β Β Β Β βœ•Β Β Β Β  Callstack


react-native-runtimes is built in collaboration with our friends at Callstack

We love fast apps ⚑ and cars 🏎️


What is this?

React Native gives your product one main JavaScript runtime. When a feed, chat screen, editor, reducer, or hydration job monopolizes that runtime, input, animation, and navigation all start competing for the same VM.

react-native-runtimes adds a multi-runtime layer for React Native New Architecture apps:

  • mount selected React components in named secondary Hermes runtimes
  • run whole screens, headless tasks, and typed functions away from the main runtime
  • share state across isolated JS heaps through a native C++ singleton
  • prewarm runtimes before navigation so expensive surfaces are ready when users arrive
  • wire it through Metro and, for Expo, a config plugin instead of hand-written runtime plumbing
// That's it. This component now renders in its own Hermes instance.
<OnRuntime name="chat-runtime">
  <MessageList conversationId={conversationId} />
</OnRuntime>

What it unlocks

If your app has...You can move...Main runtime keeps...
A chat, feed, or inbox that janks on mountThe expensive list or route surfaceNavigation, gestures, and input
Reducers or stores competing with animationBusiness logic in a long-lived runtimeFrame-critical UI work
Slow first-open screensA prewarmed runtime before the user tapsPredictable navigation latency
Background hydration or decodingHeadless tasks on a worker runtimeResponsive startup and render
State that must be visible everywhereNative-backed shared storesSynchronous reads without bridge round-trips

When to reach for it

  • You have one or two expensive features that repeatedly monopolize the main JS runtime.
  • You want a chat, feed, editor, map sidebar, or media-heavy route to stay alive and warm.
  • You need business logic or cache hydration to run without blocking interaction.
  • You are already on React Native New Architecture with Hermes, or you are willing to move there.

When not to use it

  • Your app is simple enough that memoization, virtualization, or moving work off render fixes the issue.
  • You need legacy architecture or a non-Hermes JS engine.
  • You want to pass large mutable objects or non-serializable props directly between runtimes. Pass ids/keys and read shared data from native-backed state instead.

Packages

PackageDescription
@react-native-runtimes/coreMount React components in secondary runtimes. Metro transform, OnRuntime, ThreadedScreen, headless tasks, cross-runtime function calls.
@react-native-runtimes/stateZustand-style shared store backed by a process-wide C++ singleton. Synchronous reads and commits from every runtime.

Core capabilities

🧡 Zero-boilerplate threaded components

Wrap a component in OnRuntime β€” Metro rewrites the JSX to a registered threaded boundary at build time. No manual registration required.

import { OnRuntime } from '@react-native-runtimes/core';

<OnRuntime name="feed-runtime">
  <HeavyFeedList userId={userId} />
</OnRuntime>

πŸ“± Full-screen threaded routes

For navigation flows that should live entirely on a secondary runtime:

import { ThreadedScreen, threadedComponent } from '@react-native-runtimes/core';

export const ConversationScreen = threadedComponent<Props>(
  'ConversationScreen',
  (props) => <ConversationRoute {...props} />,
);

// In your navigator:
<ThreadedScreen
  component={ConversationScreen}
  props={{ conversationId }}
  runtimeName={`chat-${conversationId}`}
/>

⚑ Runtime prewarming

Start the runtime before the user navigates so there is no cold-start lag:

import { ThreadedRuntime } from '@react-native-runtimes/core';

// e.g. when the inbox row becomes visible
await ThreadedRuntime.prewarm(`chat-${conversationId}`);

πŸƒ Headless tasks

Run JS on a named runtime without mounting a view β€” perfect for pre-hydrating stores, decoding data, or running reducers in a long-lived worker:

// Register on the threaded bundle side:
registerThreadedHeadlessTask('hydrateConversation', async ({ payload }) => {
  const messages = await loadMessages(payload.conversationId, payload.limit);
  await messagesStore.setSubtreeState(payload.conversationId, messages, true);
});

// Dispatch from anywhere:
await ThreadedRuntime.runHeadlessTask('hydrateConversation', {
  runtimeName: 'chat-worker-runtime',
  payload: { conversationId, limit: 50 },
});

πŸ”€ Cross-runtime function calls

Call a typed function on a specific runtime and await the result β€” arguments and return values are JSON-serialized automatically:

import { call, runtimeFunction } from '@react-native-runtimes/core';

export const fibonacci = runtimeFunction((n: number) => ({
  input: n,
  result: fibonacciNumber(n),
  computedAt: new Date().toISOString(),
}));

// Call it on a named runtime from the main runtime:
const result = await call(fibonacci).on('fibonacci-worker-runtime')(38);

Or use a function directive for fixed-runtime helpers β€” Metro rewrites the call site automatically:

async function refreshCache(key: string) {
  'background'; // ← directive: this function always runs on 'background' runtime
  await cacheStore.hydrate();
  return cacheStore.get(key);
}

const value = await refreshCache('settings'); // cross-runtime, no extra API

πŸ—‚ Shared state β€” synchronous, cross-heap

A Zustand-style API backed by a native C++ process-wide singleton. Reads are synchronous. No bridge round-trip. Any runtime can write and every subscriber is notified.

import { createSharedStore } from '@react-native-runtimes/state';

export const chatStore = createSharedStore({
  name: 'chat',
  initialState: { messages: {}, settings: { theme: 'dark' } },
});

// Path handles for fine-grained subscriptions:
const roomMessages = chatStore.path<Message[]>(['messages', 'release-room']);

await roomMessages.update(items => [...(items ?? []), newMessage]);

// Subscribe with a selector β€” works in any runtime:
const count = roomMessages.use(items => items?.length ?? 0);

Add native persistence with a single option:

export const preferencesStore = createSharedStore({
  name: 'preferences',
  initialState: { counter: { count: 0 } },
  persist: { key: 'preferences', version: 1, subtrees: ['counter'] },
});

πŸ— Business runtimes

For an app-lifetime runtime that sees the same native modules as the main runtime, use prewarmBusinessRuntime:

ThreadedRuntime.setMainReactPackagesProvider { PackageList(this).packages }
ThreadedRuntime.prewarmBusinessRuntime(applicationContext, "business-runtime")
if (global.__THREADED_RUNTIME_ENV__?.kind === 'business-runtime') {
  require('./src/businessRuntimeEntry');
}

πŸ”Œ Expo support

@react-native-runtimes/core ships a config plugin. No manual native edits needed:

// app.config.ts
export default {
  newArchEnabled: true,
  plugins: [
    ['@react-native-runtimes/core', {
      packages: ['@react-native-runtimes/state'],
    }],
  ],
};

Getting Started

1. Install

npm install @react-native-runtimes/core @react-native-runtimes/state react-native-nitro-modules

2. Configure Metro

// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { withThreadedRuntime } = require('@react-native-runtimes/core/metro');

module.exports = withThreadedRuntime(
  mergeConfig(getDefaultConfig(__dirname), {}),
  { roots: ['App.tsx', 'src'], generatedDir: '.threaded-runtime' },
);

3. Load the generated entry

// index.js
if (global.__THREADED_RUNTIME_ENV__) {
  require('./.threaded-runtime/entry');
} else {
  require('./App');
}

4. Render

import { OnRuntime } from '@react-native-runtimes/core';

export default function App() {
  return (
    <OnRuntime name="my-runtime">
      <HeavyComponent />
    </OnRuntime>
  );
}

β†’ Full setup guide: packages/core/README.md


Requirements

RequirementSupport
React Native0.76+
ArchitectureNew Architecture required
JavaScript engineHermes
PlatformsAndroid and iOS
ExpoConfig plugin included in @react-native-runtimes/core

Documentation


Authors


MIT License · Built with ❀️ for the React Native community

library
nitro
parallel
performance
react-native
runtimes
threading
worklets

margelo/react-native-runtimes

Run heavy React Native components and business logic in isolated Hermes runtimes (without freezing your main JS thread)

TypeScript

562

96 commits

updated Aug 5, 2026

See the code

README

react-native-runtimes logo

react-native-runtimes

Run heavy React Native UI and business logic in isolated Hermes runtimes β€” without freezing your main JS thread

React Native New Architecture Expo Android iOS License: MIT

πŸ“– Docs Β· ⚑ Quick Start Β· πŸ“¦ Core Β· πŸ—‚ State


Margelo Β Β Β Β βœ•Β Β Β Β  Callstack


react-native-runtimes is built in collaboration with our friends at Callstack

We love fast apps ⚑ and cars 🏎️


What is this?

React Native gives your product one main JavaScript runtime. When a feed, chat screen, editor, reducer, or hydration job monopolizes that runtime, input, animation, and navigation all start competing for the same VM.

react-native-runtimes adds a multi-runtime layer for React Native New Architecture apps:

  • mount selected React components in named secondary Hermes runtimes
  • run whole screens, headless tasks, and typed functions away from the main runtime
  • share state across isolated JS heaps through a native C++ singleton
  • prewarm runtimes before navigation so expensive surfaces are ready when users arrive
  • wire it through Metro and, for Expo, a config plugin instead of hand-written runtime plumbing
// That's it. This component now renders in its own Hermes instance.
<OnRuntime name="chat-runtime">
  <MessageList conversationId={conversationId} />
</OnRuntime>

What it unlocks

If your app has...You can move...Main runtime keeps...
A chat, feed, or inbox that janks on mountThe expensive list or route surfaceNavigation, gestures, and input
Reducers or stores competing with animationBusiness logic in a long-lived runtimeFrame-critical UI work
Slow first-open screensA prewarmed runtime before the user tapsPredictable navigation latency
Background hydration or decodingHeadless tasks on a worker runtimeResponsive startup and render
State that must be visible everywhereNative-backed shared storesSynchronous reads without bridge round-trips

When to reach for it

  • You have one or two expensive features that repeatedly monopolize the main JS runtime.
  • You want a chat, feed, editor, map sidebar, or media-heavy route to stay alive and warm.
  • You need business logic or cache hydration to run without blocking interaction.
  • You are already on React Native New Architecture with Hermes, or you are willing to move there.

When not to use it

  • Your app is simple enough that memoization, virtualization, or moving work off render fixes the issue.
  • You need legacy architecture or a non-Hermes JS engine.
  • You want to pass large mutable objects or non-serializable props directly between runtimes. Pass ids/keys and read shared data from native-backed state instead.

Packages

PackageDescription
@react-native-runtimes/coreMount React components in secondary runtimes. Metro transform, OnRuntime, ThreadedScreen, headless tasks, cross-runtime function calls.
@react-native-runtimes/stateZustand-style shared store backed by a process-wide C++ singleton. Synchronous reads and commits from every runtime.

Core capabilities

🧡 Zero-boilerplate threaded components

Wrap a component in OnRuntime β€” Metro rewrites the JSX to a registered threaded boundary at build time. No manual registration required.

import { OnRuntime } from '@react-native-runtimes/core';

<OnRuntime name="feed-runtime">
  <HeavyFeedList userId={userId} />
</OnRuntime>

πŸ“± Full-screen threaded routes

For navigation flows that should live entirely on a secondary runtime:

import { ThreadedScreen, threadedComponent } from '@react-native-runtimes/core';

export const ConversationScreen = threadedComponent<Props>(
  'ConversationScreen',
  (props) => <ConversationRoute {...props} />,
);

// In your navigator:
<ThreadedScreen
  component={ConversationScreen}
  props={{ conversationId }}
  runtimeName={`chat-${conversationId}`}
/>

⚑ Runtime prewarming

Start the runtime before the user navigates so there is no cold-start lag:

import { ThreadedRuntime } from '@react-native-runtimes/core';

// e.g. when the inbox row becomes visible
await ThreadedRuntime.prewarm(`chat-${conversationId}`);

πŸƒ Headless tasks

Run JS on a named runtime without mounting a view β€” perfect for pre-hydrating stores, decoding data, or running reducers in a long-lived worker:

// Register on the threaded bundle side:
registerThreadedHeadlessTask('hydrateConversation', async ({ payload }) => {
  const messages = await loadMessages(payload.conversationId, payload.limit);
  await messagesStore.setSubtreeState(payload.conversationId, messages, true);
});

// Dispatch from anywhere:
await ThreadedRuntime.runHeadlessTask('hydrateConversation', {
  runtimeName: 'chat-worker-runtime',
  payload: { conversationId, limit: 50 },
});

πŸ”€ Cross-runtime function calls

Call a typed function on a specific runtime and await the result β€” arguments and return values are JSON-serialized automatically:

import { call, runtimeFunction } from '@react-native-runtimes/core';

export const fibonacci = runtimeFunction((n: number) => ({
  input: n,
  result: fibonacciNumber(n),
  computedAt: new Date().toISOString(),
}));

// Call it on a named runtime from the main runtime:
const result = await call(fibonacci).on('fibonacci-worker-runtime')(38);

Or use a function directive for fixed-runtime helpers β€” Metro rewrites the call site automatically:

async function refreshCache(key: string) {
  'background'; // ← directive: this function always runs on 'background' runtime
  await cacheStore.hydrate();
  return cacheStore.get(key);
}

const value = await refreshCache('settings'); // cross-runtime, no extra API

πŸ—‚ Shared state β€” synchronous, cross-heap

A Zustand-style API backed by a native C++ process-wide singleton. Reads are synchronous. No bridge round-trip. Any runtime can write and every subscriber is notified.

import { createSharedStore } from '@react-native-runtimes/state';

export const chatStore = createSharedStore({
  name: 'chat',
  initialState: { messages: {}, settings: { theme: 'dark' } },
});

// Path handles for fine-grained subscriptions:
const roomMessages = chatStore.path<Message[]>(['messages', 'release-room']);

await roomMessages.update(items => [...(items ?? []), newMessage]);

// Subscribe with a selector β€” works in any runtime:
const count = roomMessages.use(items => items?.length ?? 0);

Add native persistence with a single option:

export const preferencesStore = createSharedStore({
  name: 'preferences',
  initialState: { counter: { count: 0 } },
  persist: { key: 'preferences', version: 1, subtrees: ['counter'] },
});

πŸ— Business runtimes

For an app-lifetime runtime that sees the same native modules as the main runtime, use prewarmBusinessRuntime:

ThreadedRuntime.setMainReactPackagesProvider { PackageList(this).packages }
ThreadedRuntime.prewarmBusinessRuntime(applicationContext, "business-runtime")
if (global.__THREADED_RUNTIME_ENV__?.kind === 'business-runtime') {
  require('./src/businessRuntimeEntry');
}

πŸ”Œ Expo support

@react-native-runtimes/core ships a config plugin. No manual native edits needed:

// app.config.ts
export default {
  newArchEnabled: true,
  plugins: [
    ['@react-native-runtimes/core', {
      packages: ['@react-native-runtimes/state'],
    }],
  ],
};

Getting Started

1. Install

npm install @react-native-runtimes/core @react-native-runtimes/state react-native-nitro-modules

2. Configure Metro

// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { withThreadedRuntime } = require('@react-native-runtimes/core/metro');

module.exports = withThreadedRuntime(
  mergeConfig(getDefaultConfig(__dirname), {}),
  { roots: ['App.tsx', 'src'], generatedDir: '.threaded-runtime' },
);

3. Load the generated entry

// index.js
if (global.__THREADED_RUNTIME_ENV__) {
  require('./.threaded-runtime/entry');
} else {
  require('./App');
}

4. Render

import { OnRuntime } from '@react-native-runtimes/core';

export default function App() {
  return (
    <OnRuntime name="my-runtime">
      <HeavyComponent />
    </OnRuntime>
  );
}

β†’ Full setup guide: packages/core/README.md


Requirements

RequirementSupport
React Native0.76+
ArchitectureNew Architecture required
JavaScript engineHermes
PlatformsAndroid and iOS
ExpoConfig plugin included in @react-native-runtimes/core

Documentation


Authors


MIT License · Built with ❀️ for the React Native community

library
nitro
parallel
performance
react-native
runtimes
threading
worklets

Significant stargazers

Marc Rousavy

5,285 followers Β· starred Jun 2026

Bartosz Kaszubowski

468 followers Β· starred Jun 2026

Hyeseong Kim

882 followers Β· starred Jun 2026

SΓ©bastien Lorber

2,124 followers Β· starred Jun 2026