.md

Architecture

Three runtimes — your Solid app, an embedded JS engine, and Flutter — sharing one region of memory. Here's the whole path from a signal write to a pixel.

The three layers

  • Your app (SolidJS). Plain Solid components compiled against Skal's universal renderer. Instead of creating DOM nodes, the renderer emits binary operations.
  • libskal (bun + JavaScriptCore). A native library embedding a slice of bun — its JSC runtime, module loader, bytecode cache, and Skal's store engine — inside your app's process. Your bundle runs here, on a worker thread.
  • The Flutter host. A thin Dart layer that drains the op ring once per frame tick and maintains a widget tree mirroring your component tree. Rendering, gestures, accessibility, and plugins are stock Flutter.

The shared region

At startup, libskal allocates a single 6 MiB block that both sides address directly for the app's whole lifetime:

SegmentSizeDirectionCarries
Header64 Bbothcursors, sequence counters, reset epochs
Op ring4 MiBJS → DartCREATE_NODE, INSERT_BEFORE, SET_PROP_*, SET_TEXT
String heap768 KiBJS → DartUTF-8 payloads referenced by string ops
Reply heap256 KiBDart → JSRPC results (dialogs, platform calls)
Event ringrestDart → JStaps, scrolls, text input, lifecycle

JS sees the block as a Uint8Array over externally-owned bytes (JSC's no-copy ArrayBuffer); Dart sees the same bytes as a typed list over an FFI pointer. There is no serialization step anywhere — "crossing the bridge" is writing integers into a ring buffer.

Life of an update

  1. You tap a <Button>. Flutter's gesture arena resolves the tap and writes one small event (handler id + kind) into the event ring, then wakes the JS thread.
  2. Skal's dispatcher reads the event and invokes your onClick. A signal updates.
  3. Solid's reactivity re-runs only the computations that read that signal. Each affected prop becomes one op in the op ring — a 5,000-row list where one label changed emits exactly one op.
  4. On the next frame tick, Dart drains the ring (measured at 0.151 ms per frame, steady state), patches its node table, and marks precisely the touched widgets dirty via per-node notifiers.
  5. Flutter rebuilds those widgets — and only those — and composites the frame.

Hot props (opacity, transforms) take an even shorter path: the host tweens them on its own animation ticker, so a 60 fps animation costs zero bridge traffic per frame.

Cold start & bytecode

Release builds ship the bundle twice: as source (.cjs) and as pre-compiled JavaScriptCore bytecode (.jsc). On launch, JSC attaches the bytecode directly — the parser never runs. That turns a several-hundred-millisecond parse into a 36 ms eval, and a 56 ms cold boot to first frame.

The bytecode is keyed to the exact JSC version inside libskal — the toolchain builds both from the same pinned runtime, so they can never skew.

Why not a virtual DOM? Why not Flutter alone? Why not RN?

  • vs. React Native: RN's architecture pays for serialization and a JS-side shadow tree. Skal's renderer writes binary ops straight into memory Flutter reads — the "bridge" is a pointer.
  • vs. Flutter alone: you keep the JS ecosystem, sub-second hot reload of app code, and a language your web team already ships in — while the rendering fidelity IS Flutter.
  • vs. WebViews: nothing here is a browser. Widgets are real platform-composited Flutter widgets; the DOM never existed.