Headless background runtime with prewarm
Run JS on a named runtime without mounting UI — fetch, hydrate, decode, or update shared state before a screen opens.
A headless task runs JS on a named threaded runtime without mounting any UI. Use it when a runtime should fetch, hydrate, decode, or update shared state before a screen opens.
The same mechanism can also run heavier business logic away from the main JS runtime. See Background thread architecture for a crypto-style job queue example.
Register a task
Register inside code loaded by the threaded bundle — typically a per-runtime bootstrap file:
import { registerThreadedHeadlessTask } from '@react-native-runtimes/core';
import { messagesStore } from './messagesStore';
registerThreadedHeadlessTask<{
conversationId: string;
limit: number;
}>('hydrateConversation', async ({ payload, runtimeName }) => {
const messages = await loadMessages(payload.conversationId, payload.limit);
await messagesStore
.path<Message[]>(['conversations', payload.conversationId])
.set(messages, true);
console.info(`Hydrated ${payload.conversationId} on ${runtimeName}`);
});Dispatch from JS
import { ThreadedRuntime } from '@react-native-runtimes/core';
await ThreadedRuntime.runHeadlessTask('hydrateConversation', {
runtimeName: 'conversation-worker-runtime',
payload: {
conversationId: 'release-room',
limit: 50,
},
});Native starts or reuses the named runtime. If the runtime is still starting, the task is queued and flushed after startup.
The promise resolves on dispatch, not completion
runHeadlessTask resolves when native accepts the dispatch, not when
the async task body finishes. Use shared state to observe progress, or use
an awaitable runtime function for request/response work.
Await runtime functions instead
For request/response work, use an awaitable runtime function:
import { call, runtimeFunction } from '@react-native-runtimes/core';
export const hydrateConversation = runtimeFunction(
async ({ conversationId }) => {
const messages = messagesStore.path<Message[]>([
'conversations',
conversationId,
]);
await messages.hydrate();
return messages.get();
},
);
const messages = await call(hydrateConversation).on(
'conversation-worker-runtime',
)({ conversationId: 'inbox' });The Metro wrapper assigns the exported function a stable id and rewrites the
call(...).on(...) call to dispatch it to the requested runtime. See
Scheduling functions on another runtime
for the full API contract and lookup model.
Native dispatch
When JS isn't a convenient caller — for example from a foreground service or a notification handler — dispatch from native code instead.
ThreadedRuntime.dispatchHeadlessTask(
context = applicationContext,
runtimeName = "conversation-worker-runtime",
taskName = "hydrateConversation",
payloadJson = """{"conversationId":"release-room","limit":50}""",
)ThreadedRuntime.dispatchHeadlessTask(
withRuntimeName: "conversation-worker-runtime",
taskName: "hydrateConversation",
payloadJson: #"{"conversationId":"release-room","limit":50}"#
)#include <nativecompose/threadedruntime/ThreadedRuntimeDispatcher.h>
nativecompose::threadedruntime::dispatchHeadlessTask(
env,
applicationContext,
"conversation-worker-runtime",
"hydrateConversation",
R"({"conversationId":"release-room","limit":50})"
);#include <nativecompose/threadedruntime/ThreadedRuntimeDispatcher.h>
nativecompose::threadedruntime::dispatchHeadlessTask(
"conversation-worker-runtime",
"hydrateConversation",
R"({"conversationId":"release-room","limit":50})"
);Typical flow
- Prewarm the runtime while the user is still on the previous screen.
- Dispatch a headless hydration task to that runtime.
- Store durable output in shared state or native storage.
- Open a
ThreadedScreenusing the same runtime name. - The screen reads the already-warmed data from the shared store.
Why this layered approach helps
By the time the user navigates, the runtime is alive, the data is loaded, and the screen mounts against a populated store. The transition feels instant.