<!-- Skal docs — markdown mirror of https://skal.run/docs/architecture.html -->

# 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:

| Segment | Size | Direction | Carries |
| --- | --- | --- | --- |
| Header | 64 B | both | cursors, sequence counters, reset epochs |
| Op ring | 4 MiB | JS → Dart | `CREATE_NODE`, `INSERT_BEFORE`, `SET_PROP_*`, `SET_TEXT`… |
| String heap | 768 KiB | JS → Dart | UTF-8 payloads referenced by string ops |
| Reply heap | 256 KiB | Dart → JS | RPC results (dialogs, platform calls) |
| Event ring | rest | Dart → JS | taps, 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.

[Previous← Getting Started](./) [NextComponents →](components.html)
