Lynx/Modules/Main Runtime/Usage
@sigx/lynx-runtime-main · Stable

Using Main Runtime#

The main-thread (Lepus) runtime that applies the background-to-main op stream, runs worklets and drives main-thread refs. You rarely call it by hand — this guide is for when you do.

Basic usage#

Application code almost never imports @sigx/lynx-runtime-main directly. The @sigx/lynx-plugin build plugin wires it into the main-thread bundle for you, prepending the entry imports in the exact order the runtime needs. The barrel entry re-exports the named API and, as a side effect, installs the renderer hooks on globalThis.

TSX
import * as MainRuntime from '@sigx/lynx-runtime-main';

The package ships three entry points, each with a distinct job:

TypeScript
// 1. Named API + entry-main side effect (installs renderer hooks).
import { applyOps, MTElementWrapper } from '@sigx/lynx-runtime-main';

// 2. Side-effect-only: installs the renderer hooks alone.
import '@sigx/lynx-runtime-main/entry-main';

// 3. Side-effect-only: registers the hybrid event dispatcher.
import '@sigx/lynx-runtime-main/install-hybrid-worklet';

This is a pure-TypeScript Lepus runtime — there is no native iOS or Android module to link. It calls Lynx PAPI globals (__CreateView, __SetInlineStyles, __AddEvent, __FlushElementTree, and friends) that the Lynx host provides at run time. @lynx-js/types is an optional peer dependency if you want the ambient PAPI typings.

Bootstrap order#

When you assemble the main-thread bundle yourself, the three entry points must evaluate in this order:

TypeScript
// 1. Renderer hooks + SystemInfo (must run before the worklet runtime IIFE).
import '@sigx/lynx-runtime-main/entry-main';
// 2. Upstream worklet runtime: populates lynxWorkletImpl, runWorklet, etc.
import '@lynx-js/react/worklet-runtime';
// 3. Hybrid dispatcher: registers into the now-populated worklet map.
import '@sigx/lynx-runtime-main/install-hybrid-worklet';

The hybrid dispatcher lives in its own side-effect module on purpose: a bare import would otherwise be hoisted above the worklet runtime it depends on. @sigx/lynx-plugin prepends these imports for you, so you only need this when hand-rolling the bundle.

Driving an element from a main-thread worklet#

The most common reason to reach into this runtime is a MainThreadRef. The runtime sets the ref's .current to an MTElementWrapper, giving a main-thread worklet a synchronous, zero-latency handle to read and write styles, attributes, run query selectors, invoke UI methods and start animations.

TSX
// Inside a main-thread worklet body.
function onScroll(ref: MainThreadRef, offset: number, ratio: number) {
  'main thread';
  ref.current?.setStyleProperties({
    transform: `translateX(${offset}px)`,
    opacity: `${1 - ratio}`,
  });
}

Writes are batched: every setStyleProperties, setStyleProperty, setAttribute and invoke call across all wrapper instances is microtask-debounced into a single __FlushElementTree, so a tick of updates costs one native flush.

Running keyframe animations#

MTElementWrapper.animate starts a native keyframe animation and returns a small controller you can play, pause or cancel.

TSX
function fadeIn(ref: MainThreadRef) {
  'main thread';
  const anim = ref.current?.animate(
    [{ opacity: 0 }, { opacity: 1 }],
    { duration: 300, easing: 'ease-in-out', fill: 'forwards' },
  );
  anim?.play();
}

animate returns null if the underlying element handle is unavailable, so guard the result before calling controller methods.

Reading state back from the element#

The wrapper also exposes synchronous read accessors and a promise-based UI-method bridge.

TSX
function measure(ref: MainThreadRef) {
  'main thread';
  const el = ref.current;
  if (!el) return;

  const width = el.getComputedStyleProperty('width');
  const tag = el.getAttribute('data-role');
  const names = el.getAttributeNames();
}

async function scrollToTop(ref: MainThreadRef) {
  'main thread';
  await ref.current?.invoke('scrollTo', { offset: 0, smooth: true });
}

invoke returns a Promise, so it can be awaited; the synchronous accessors return immediately from the live PAPI handle.

Calling helpers from a worklet#

A worklet reaches the module-level bindings it names through a capture that crosses from the background thread to the main thread as JSON. Only some values survive that trip:

  • A 'main thread' function crosses as a worklet reference that the main-thread runtime makes callable. A worklet can call it.
  • A plain function is dropped. Calling it on the main thread throws TypeError: not a function.
  • A method called on a captured object is dropped too. The capture copies only the members the worklet touches, so ALLOWED.indexOf(x) ships indexOf as a captured function and throws the same way. Spell the check out instead.
  • Plain data (numbers, strings, arrays and objects of those) is copied — into each worklet separately, so a plain object is not shared state between two worklets.

So a helper a worklet calls must itself carry the 'main thread' directive:

TSX
function clamp(v: number, lo: number, hi: number) {
  'main thread';
  return Math.min(hi, Math.max(lo, v));
}

function onUpdate(ref: MainThreadRef, x: number) {
  'main thread';
  ref.current?.setStyleProperty('opacity', `${clamp(x / 200, 0, 1)}`);
}

The reverse holds as well: a 'main thread' function is a worklet reference on the background thread, so call it from worklet code only. useScreenMT, useWidthClassMT and useHeightClassMT from @sigx/lynx-core follow this rule.

Notes#

  • Object-prototype rebuild (PrimJS): when a runOnMainThread call carries captured values, the runtime rebuilds the payload with real object prototypes before handing it to the upstream worklet runtime. PrimJS's JSON.parse yields null-prototype objects that its WeakRef rejects, which would otherwise make captured animations silently no-op. This is handled for you; it is the reason captured worklet payloads work at all.
  • Many internal helpers (animated-style mappers, the AV bridge, list ops, the run-on-background dispatcher) are not on the public export surface — they are wired up through the entry-main side effect rather than imported by name.

See also#