Parallel conveyor belts moving glowing code parcels between workstations, with a large red Maine Coon cat resting nearby.

React Native Worklets: Threading the Needle Across Runtimes

The Orange Cat
The Orange Cat

If you have ever watched a React Native animation stutter while your app parses a big JSON payload, you have met the single-threaded JavaScript bottleneck head on. Everything your app does in JavaScript runs on one thread, and the moment you ask it to do something heavy, animations drop frames and gestures feel sticky. react-native-worklets is Software Mansion's answer to that problem: a multithreading library that lets you run JavaScript on the UI thread, on dedicated background threads, or on custom runtimes you spin up yourself, all without dropping down to C++ or writing a native module.

The trick is a small directive. Mark a function with 'worklet'; (or let the Babel plugin do it for you), and that function, along with a serialized copy of the data it closes over, can be shipped to and executed on another thread. This is the exact engine that powers Reanimated, and with Reanimated 4 it was extracted into its own package so you can use it directly. If you build animations, gesture handlers, audio processors, or anything CPU-heavy, this library gives you roughly eighty percent of native multithreading's benefit while you stay in JavaScript.

What You Get in the Box

The library is small in surface area but big in capability. The headline features are:

  • A UI thread runtime so animation and gesture logic can run at 60 to 120 FPS in lockstep with rendering, independent of your JS thread.
  • Custom background runtimes created with createWorkletRuntime, each running on its own thread, like a poor-man's Web Worker for React Native.
  • Structured cross-thread scheduling with scheduleOnUI, scheduleOnRN, and scheduleOnRuntime to move work between runtimes in a predictable, ordered way.
  • Shared values that can be read and written safely from multiple runtimes, so data survives the trip across the thread boundary.
  • Automatic workletization via a Babel plugin that adds the 'worklet'; directive for you in common cases, cutting boilerplate.

Because it is the foundation layer beneath Reanimated, Gesture Handler, Skia, and Live Markdown, it is also battle tested at enormous scale. It ships transitively in a large fraction of React Native apps through Reanimated 4.

Getting It Into Your Project

Install the package with your manager of choice:

npm install react-native-worklets
yarn add react-native-worklets

If you are on the React Native Community CLI, add the Babel plugin to babel.config.js. It should be listed last, otherwise you will see confusing "function is not a worklet" errors at runtime:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    // ...your other plugins
    'react-native-worklets/plugin',
  ],
};

On Expo, run npx expo prebuild to rebuild native dependencies. On iOS, finish with cd ios && pod install. Android needs no extra steps. One important requirement: Worklets targets the New Architecture (Fabric). It is not tested on the legacy Paper architecture, so migrate to Fabric before you adopt it.

Crossing the Thread Boundary

The most common thing you will do is bounce a piece of work from your JS thread onto the UI thread and back. A worklet is just a function carrying the 'worklet'; directive:

function greetFromUI() {
  'worklet';
  console.log('Running on the UI Runtime, in sync with rendering');
}

To run a worklet on the UI thread from your normal React code, use scheduleOnUI (the modern replacement for the older runOnUI). This is how you reach UI-thread-only operations like measuring layout or scrolling:

import { scheduleOnUI } from 'react-native-worklets';

function App() {
  scheduleOnUI((greeting: string) => {
    'worklet';
    console.log(`${greeting} from the UI thread`);
  })('Howdy');
}

Going the other direction, when you are on the UI thread and need to touch React state, navigation, or any library that is not workletized, use scheduleOnRN (formerly runOnJS). The RN in the name stands for the React Native runtime, where your normal code lives:

import { scheduleOnRN } from 'react-native-worklets';

function handleGestureEnd(setValue: (n: number) => void) {
  'worklet';
  // We are on the UI thread here, so bounce back to the RN runtime
  scheduleOnRN(setValue, 10);
}

There is one footgun worth burning into memory. Always pass the function reference and then call the returned function with the arguments. scheduleOnRN(setValue, 10) is correct; scheduleOnRN(setValue(10)) is a bug, because it calls setValue immediately on the wrong thread.

Sharing Data, Not Memory

Each runtime has its own memory. When a worklet captures variables, that closure is serialized and copied into the target runtime rather than shared. Mutating a captured object on the UI thread will not be reflected on the JS thread. This is exactly why shared values exist: they are mutable containers that any runtime can read and write safely, and they are the same mechanism Reanimated uses to drive its animations.

The practical takeaway is to keep your captured closures small. Capturing a large object means copying that large object across the boundary every time. When you need ongoing, two-way communication, reach for shared values and the scheduleOn* bridges instead of trying to mutate a captured reference.

Spinning Up Background Runtimes

Where Worklets really shines beyond animation is offloading heavy work onto a dedicated background thread. You create a new runtime with createWorkletRuntime, then schedule worklets onto it. This keeps both your JS thread and your UI thread free while expensive computation runs elsewhere:

import {
  createWorkletRuntime,
  scheduleOnRuntime,
  scheduleOnRN,
} from 'react-native-worklets';

const backgroundRuntime = createWorkletRuntime('background');

function processInBackground(onDone: (result: number) => void) {
  scheduleOnRuntime(backgroundRuntime, (count: number) => {
    'worklet';
    let total = 0;
    for (let i = 0; i < count; i++) {
      total += Math.sqrt(i);
    }
    // Hand the result back to React for rendering
    scheduleOnRN(onDone, total);
  })(5_000_000);
}

Two things to note. First, functions passed to scheduleOnRuntime are not auto-workletized, so you must add the 'worklet'; directive yourself, unlike many Reanimated callbacks. Second, worklets scheduled on a runtime execute in the order they were queued, which gives you predictable sequencing for pipelines of background tasks.

A Word on the Evolving API

This library is still in its 0.x line, version 0.9.1 at the time of writing, and the API is actively shifting. The older Reanimated-era names are deprecated and slated for removal in the next major release. If you have existing code or follow older tutorials, here is the mapping:

Deprecated name Modern replacement
runOnUI scheduleOnUI
runOnJS scheduleOnRN
runOnRuntime scheduleOnRuntime

Both sets currently work, but new code should use the schedule* family. Because the package is pre-1.0, pin your version and expect occasional breaking changes between releases. The upside is that maintenance risk is low: it is built and funded by Software Mansion, ships nightly builds, and sees roughly 3.66 million weekly downloads, mostly riding along with Reanimated 4.

Bridging Worklets and Reanimated

If you are already on Reanimated 4, you have Worklets in your dependency tree whether you imported it directly or not. That makes it natural to use the same primitives for non-animation work. Imagine a gesture that computes a value on the UI thread and then needs to push that value into React state for a network request:

import { scheduleOnRN } from 'react-native-worklets';
import { Gesture } from 'react-native-gesture-handler';

function buildGesture(setOffset: (x: number) => void) {
  return Gesture.Pan().onUpdate((event) => {
    'worklet';
    // Runs on the UI thread for buttery-smooth tracking,
    // then syncs the final value back to the RN runtime.
    scheduleOnRN(setOffset, event.translationX);
  });
}

The gesture callback runs entirely on the UI thread, so dragging stays smooth even if the JS thread is busy. Only when you genuinely need React to know about the value do you cross back with scheduleOnRN. This pattern, doing the frame-critical work on the UI runtime and syncing sparingly, is the heart of writing performant React Native interactions.

Wrapping Up

react-native-worklets takes one of the hardest problems in React Native, true multithreading, and makes it approachable from plain JavaScript. With a 'worklet'; directive, a Babel plugin, and a tidy set of scheduleOn* functions, you can run animation logic in sync with rendering, push heavy computation onto background runtimes, and keep your UI responsive no matter what your JS thread is doing. The mental model takes a moment to internalize, especially the copy-not-share semantics and the function-reference footgun, but once it clicks you have a powerful, first-party tool for performance.

It is not for everyone: apps stuck on the legacy architecture are out of scope, and if your JS thread is not actually a bottleneck you may be adding complexity you do not need. But for anyone chasing frame-perfect gestures, smooth 120 FPS animation, or background processing without writing a native module, this is the engine that the rest of the Software Mansion ecosystem already trusts. Reach for it, pin your version, and enjoy the extra threads.