[ Documentation ]
The Runtime

The Runtime

On a phone, React Native splits your app across two threads. The JS thread runs your bundle: React renders, your event handlers and your state updates. The UI thread owns what the user sees and touches: scrolling, touch recognition, native-driver animations and Reanimated worklets. The split is why a busy JS thread makes your app slow to respond but does not freeze a scroll in progress.

Peach keeps that split. Your app runs across separate workers in the browser, and each one owns the same work its counterpart owns on a device. This page covers what that means when you write and debug an app. For the full engine design, read How Peach works.

Where your code runs

on a devicein Peachowns
JS threadtenant workeryour bundle, React, layout, event handlers, TextInput, accessibility, network and storage
UI threadshell workerscrolling, gesture recognition, native-driver animations, Reanimated worklets, system UI such as the status bar, keyboard and alerts
render servercompositor workerpainting every visible pixel with Skia on the GPU, at its own frame rate

The page itself presents the canvases the compositor paints and routes your pointer input. None of these workers waits on another to draw a frame, so a stall in one does not stop the others.

Your bundle runs on the browser’s own JavaScript engine inside the tenant worker. Layout is computed with Yoga in that same worker, as React Native computes it for Fabric, and the results are shared with the other workers, so a worklet’s measure() reads the same numbers your JS code sees.

What keeps moving when your JavaScript is blocked

A long synchronous loop, a heavy render or a debugger pause stops the tenant worker. On a phone that freezes your JS thread, and Peach behaves the same way:

Keeps moving

  • +
    Scrolling, including momentum and bounce
  • +
    Animated animations started with useNativeDriver: true
  • +
    Reanimated animations and useAnimatedStyle updates that run on the UI runtime, including Reanimated CSS animations of opacity and transforms
  • +
    Gesture Handler recognition and gesture worklets
  • +
    System UI: the status bar clock, the keyboard, a spinning ActivityIndicator, the home screen and app switcher

Waits for your JavaScript

  • +
    onPress and every other JS event handler
  • +
    setState, re-renders and layout changes
  • +
    Animated animations with useNativeDriver: false, which compute every frame on the JS thread
  • +
    runOnJS callbacks from worklets and gestures

When your JavaScript finishes, the queued work runs in order, as it does on device. If an interaction feels stuck in Peach, check the same thing you would check on a phone: whether that animation or gesture runs on the UI thread or waits on JS.

useNativeDriver

useNativeDriver: true moves an animation to the UI thread. In Peach your Animated.timing, Animated.spring or Animated.decay is described once, sent to the shell worker and stepped there on its own clock. The compositor paints each frame from the values the shell writes. The tenant worker does no work per frame, which is why a native-driver animation keeps running while your JavaScript is blocked.

Two React Native rules carry over unchanged:

  • +
    A timing curve is sampled into frames up front, as React Native does before handing a native animation to the platform, so a custom easing function runs on the native driver too.
  • +
    The native driver animates only non-layout properties: opacity, transforms and colors. Animating width, height, top or other layout props needs useNativeDriver: false, as it does on device.

To give an Animated.spring the exact feel of an iOS spring, convert SwiftUI or UIKit spring parameters with spring() from peach-kit/animation.

Reanimated and worklets

Reanimated’s UI runtime is the shell worker. Worklets you run on the UI thread, the mappers behind useAnimatedStyle and useDerivedValue, and Reanimated event handlers such as useAnimatedScrollHandler all execute there, next to the scroll and gesture state they read. That matches where they run on device.

  • +
    runOnUI queues work on the shell, as on device.
  • +
    runOnUISync and executeOnUIRuntimeSync block your JS thread until the shell returns its result, so you get the UI runtime’s real value back synchronously.
  • +
    runOnJS hands work back to your JS thread.

Shared values cross between the two runtimes the way they do on device: a write from the UI runtime is visible to your JS code, and a write from JS reaches the worklets that read it.

Gestures

React Native Gesture Handler recognizes gestures on the shell worker, against a mirror of your view tree, from the same pointer stream the system gestures use. Worklet callbacks on a gesture run on the UI runtime immediately. Callbacks that run on JS, either with runOnJS(true) or through runOnJS, are delivered to your tenant worker in order. Native interactions such as swipe-back, sheet drags and scroll views negotiate with your gesture handlers as UIKit and Android do: the recognizer that activated first keeps the touch.

Rendering

The compositor paints with Skia (CanvasKit) on the GPU, through WebGPU where the browser supports it and WebGL otherwise. Each app, the home screen and the system overlay paint to their own surfaces, and the compositor repaints only the surfaces that changed.

  • +
    How Peach works: the worker model, the render pipeline and why stalls never freeze the UI
  • +
    Library compatibility: how much of Reanimated, Gesture Handler and other libraries Peach implements
  • +
    peach perf: measure frame timing in your app