miso

Native (mobile)

The dual-thread architecture

Lynx runs your application across two threads, and miso maps onto both:

  • BTS — the background thread ("background thread script"). This is where your application logic lives. Everything runs here by default: the update function, event handling, Effect scheduling and all virtual-DOM diffing.
  • MTS — the main thread ("main thread script"). This thread owns the actual element tree and rendering. It is where the pixels land. It is also available as a low-latency escape hatch for performance-critical event handling (see Main-thread events).

The same Haskell bundle runs on both threads; the native runtime selects the BTS or MTS drawing context per thread from a global flag, so there is no renderer to register — native starts the app directly. The guiding principle: everything originates on the BTS. The MTS is a rendering surface that the BTS drives across the thread boundary.

Knowing which thread you are on

Lynx builds the bundle with rspeedy, which compiles the sources twice, once per thread, inlining a compile-time constant (__BACKGROUND__) that distinguishes the two. That constant surfaces in Haskell as three top-level Bools in Miso.Runtime (re-exported from Miso):

mts
True when this execution context is the Lynx main thread.
bts
True when this context is the Lynx background thread.
web
True for a plain web / WASM build (neither Lynx thread).

Exactly one is True and the value is invariant for the lifetime of a JS context, so the runtime computes it once and caches it. Runtime code branches on mts / bts to decide where work runs (e.g. the scheduler suppresses the paint step on the MTS, which keeps only a read-only model replica).

What crosses the thread boundary, and how

Because logic (BTS) and rendering (MTS) live on different threads, miso synchronises them by shipping messages across the boundary. This is largely invisible, but understanding it explains the API constraints below.

  • Initial draw — the very first draw happens on the MTS itself, and does not rely on the BTS diffing a tree and transferring patches. The root component is booted from a StaticPtr (via native), so the MTS reconstructs it from the pointer's StaticKey alone and renders the first frame locally (Lynx's instant first frame). Only after this does the cross-thread patch protocol take over: every subsequent diff runs on the BTS and ships patches to the MTS.
  • Subsequent component mounts — when the BTS view mounts a child, that mount is synchronised to the MTS asynchronously using static mounting: the child is wrapped in a static pointer so only its StaticKey — not a closure — crosses the boundary. See Static mounting.
  • State synchronisation — the BTS owns the shared model and ships it to the MTS as it changes (JSON-serialised, hence the ToJSON / FromJSON constraints on native mounting combinators), so main-thread *MainWith handlers observe an eventually-consistent copy. A child's initial props ride the static-mount payload, but props and the global context are not re-synced on later changes: after the first frame they stay background-thread-only (matching ReactLynx).
  • Events — events raised on the MTS are, by default, forwarded to the BTS where update runs. Cross-thread handlers are carried as an EventHandler embedded with event . static (…) so the peer thread can rebuild the handler from its StaticKey.

The lifecycle, in sequence

MTSBTSmiso runtimeMTSBTSmiso runtimeinitial draw (instant first frame)register BTS + MTS eventsinitialize component treecomponent mount / model hydrationreceive / process MTS eventreceive BTS eventforward BTS event (e.g. tap)update state / perform effectsnative module accessdiff + create patchesforward patcheselement updaterunOnBG action — update runs on the BTSrunOnMain action — update runs on the MTSonMain handler — handled on the MTS, no round-tripon handler — event forwarded, update on the BTS
The dual-thread Component initialization and lifecycle. The runOnBG / runOnMain effects and the on / onMain handlers (highlighted) are how work hops between the threads.

First-frame rendering (instant first frame)

The MTS painting frame one itself is Lynx's instant first frame: the user sees UI without waiting for a background render and patch round-trip. Meanwhile the BTS boots the same root and builds the identical virtual-DOM tree in lockstep — with deterministic nodeId parity, so both threads address the same elements — but suppresses its own create-patches for that first frame, since the MTS already painted them. A single global initialDraw latch governs this on both threads; native clears it once the whole root mount has finished.

After that handover the responsibilities are fixed, mirroring ReactLynx: the BTS is the sole diff / paint authority — it runs update, diffs and ships patches — while the MTS only applies those patches (and runs main-thread scripts / handlers). The MTS never diffs or repaints from the scheduler again; this is why the shared model is BTS-owned and why nothing you do on the MTS should try to redraw declaratively.