Performance-first, batteries included, every platform. Fast at your first screen. Still fast at your hundredth.
import { createSignal } from 'solid-js'; import { Column, Text, Button, Row } from 'skal'; export default function App() { const [count, setCount] = createSignal(0); return ( <Column padding={24} gap={16}> <Text label="Hello, Skal" fontSize={28} fontWeight={800} /> <Text label={count() === 0 ? 'Tap the button' : `Tapped ${count()} times`} /> <Row gap={12}> <Button label="Tap" onClick={() => setCount(c => c + 1)} /> <Button label="Reset" onClick={() => setCount(0)} /> </Row> </Column> ); }
Cross-platform JS frameworks lose their performance in the seams — serialization, VDOM diffing, JIT-less interpreters. Skal removes the seams.
Fine-grained reactivity with no virtual DOM — a signal update touches exactly the widgets that read it. The bundle ships as JSC bytecode, so cold start skips the parser entirely. And the web platform — crypto, fetch, Buffer — is built in, not polyfilled.
JS and Dart share one permanent 6 MiB memory region. UI mutations are binary ops in a ring buffer — no JSON, no MessageQueue, no copies. Both sides read the same bytes.
Pixels come from Flutter's compositor — consistent on every OS. Need camera, geolocation, biometrics? Wrap any pub.dev package with one line of codegen config.
Save a .jsx file and the running native app re-evaluates it in place — navigation stack and state preserved. Manual r or automatic on save. Compiled out of release builds.
createSkalStore is a reactive store persisted by a log-structured native engine — no Redux, writes are sub-millisecond. Navigation with Hero transitions and deep links is first-party too. Zero dependency matrix before your first screen.
bun test for framework logic, flutter test for the host, and Maestro flows driving the real app on a real device — testID just works.
Your app runs in JavaScriptCore embedded via libskal (a slice of bun). Flutter drains a binary op ring once per frame. Events flow back through the same region — watch them.
JS sees the region as a Uint8Array (zero-copy via JSC's no-copy ArrayBuffer); Dart sees the same bytes as a typed view over an FFI pointer. Deep dive in Architecture.
Head-to-head against React Native on a Samsung Galaxy A14 5G, Android 15, arm64 — release builds on both sides, runs interleaved, screen held awake. React Native 0.86 / Expo 57 with Hermes, zustand and MMKV. Method and raw data in docs/BENCHMARKS.md. If your hardware disagrees, that's a bug and we want the issue.
| What you feel | Skal | React Native |
|---|---|---|
| Dropped frames, scrolling an image feed | 0 | 13 |
| Scroll p95, image feed | 6 ms | 12 ms |
| Input-latency events while scrolling | 0 | 631 |
| Tap → render, median | 3.53 ms | 7.30 ms |
| Tap → render, p95 | 4.87 ms | 10.24 ms |
| Idle CPU | 1.20% | 6.93% |
| App size at full WebCrypto parity | 51.6 MiB | 67.2 MiB |
| State updates | Skal | React Native |
|---|---|---|
| One frame of work — 200 components × 10 reads + 1 update | 0.05 ms | 0.80 ms |
| One update, 200-key store, no subscribers | 0.0012 ms | 0.1081 ms |
| One update, 200 subscribers | 0.1425 ms | 0.2119 ms |
| 200 updates, one subscriber each | 0.63 ms | 38.15 ms |
| Update throughput, 288 live cells | 103,846/s | 4,901/s |
| Dropped frames under that pressure | 4.6% | 50.4% |
| Components woken by one change | 1 of 200 | 1 of 200 |
| — cost of that update | 0.0024 ms | 0.150 ms |
Immutable stores copy the whole state object on every write, so the gap widens with store size and narrows with subscriber count. Both ends are shown.
| JavaScript engine | Skal (JSC) | React Native (Hermes) |
|---|---|---|
| 8 JS workloads, warm | JSC 7.3× faster median, wins 8/8 | |
| — closure calls, 3k | 0.0124 ms | 0.2141 ms |
— worst case, JSON.parse | 1.1755 ms | 1.9915 ms |
| — first execution, cold | tie — JSC overtakes within ~10 runs | |
JS engine measured warm. Hermes ships pre-compiled bytecode and stays flat; JSC compiles hot code and overtakes within about ten iterations.
Everyone says "write once, run anywhere, feels native." The difference is what happens between your code and the screen.
| Skal | React Native | Flutter | |
|---|---|---|---|
| You write | Solid JSX | React JSX | Dart |
| Reactivity | signals — no VDOM, no diffing | VDOM reconciliation | rebuild + element diff |
| JS ↔ native transport | shared memory ring, zero-copy | JSI calls + serialized boundaries | — (single language) |
| JS engine | JavaScriptCore + bytecode cache | Hermes | — |
| Rendering | Flutter compositor | platform views | Flutter compositor |
| App-code hot reload | < 1 s, navigation + state kept | Fast Refresh | stateful hot reload |
| Native plugin pool | pub.dev, via codegen | npm native modules | pub.dev |
Both neighbors are excellent at what they optimize for. Skal exists for teams that want JS ergonomics and Flutter's rendering — without paying a serialization boundary between them.
The same skal-app.js runs unchanged on every target. Release builds ship bytecode; the web target renders Solid straight to the DOM — no Flutter Web required.
| Target | Status | Notes |
|---|---|---|
| Android | Stable | arm64 · JSC bytecode cache · R8-ready release path |
| iOS | Stable | device + simulator · bun/WebKit cross-compiled for aarch64-apple-ios |
| macOS | Stable | debug + release desktop shells |
| Web | Stable | Solid → DOM directly; optional headless Flutter for plugin access |
| Linux · Windows | Coming | Flutter Desktop supports both; libskal linkers in progress |
Capitalized tags from 'skal' compile to binary ops that build real Flutter widgets — layout, form controls, virtualized lists, slivers, navigation, canvas. Every snippet below is lifted from the kitchen-sink demo.
const [name, setName] = createSignal(''); <Switch checked={sw()} onChange={(v) => setSw(v)} /> <Slider value={vol()} min={0} max={100} onChange={setVol} /> <TextInput value={name()} placeholder="Type your name…" onChange={setName} onSubmit={(v) => showSnackbar(`Hi ${v}`)} />
// virtualized — 10,000 rows scroll at 60 fps <ListView gap={8} onRefresh={reload}> <For each={tweets()}> {(t) => <TweetCard tweet={t} />} </For> </ListView> // also: LazyGrid · ReorderableListView · slivers
<Tabs activeTab={tab()} onChange={setTab} height="fill"> <Tab title="UI" icon="grid"><UITab /></Tab> <Tab title="Feed" icon="list"><FeedTab /></Tab> <Tab title="Store" icon="storage"><StoreTab /></Tab> </Tabs> // screens: createRouter → navigate / back, Hero, Drawer
<Canvas width={300} height={170} draw={(c) => { vals().forEach((v, i) => c.fillStyle(i === 3 ? ACCENT : PURPLE) .fillRect(28 + i * 52, 150 - v, 34, v)); c.fillStyle(INK).fontSize(12) .fillText('live chart', 18, 22); }} />
// pub.dev packages wrapped by one line of codegen <QrImageView data="https://github.com/skal-multiplatform" size={200} /> <ShimmerFromColors baseColor={0xFFBDBDBD} highlightColor={0xFFE0E0E0}> <Greeting name="loading…" fontSize={28} /> </ShimmerFromColors>
const r = await showDialog({ title: 'Delete file?', message: 'This cannot be undone.', actions: [ { label: 'Cancel', value: 'cancel' }, { label: 'Delete', value: 'delete', style: 'destructive' }, ], });
The full set — Layout Column · Row · Stack · Wrap · Box Scrolling ScrollView · ListView · LazyGrid · PageView · slivers Input Button · TextInput · Switch · Checkbox · Radio · Chip · Dropdown · SegmentedButton · Stepper Motion AnimatedList · CrossFade · Dismissible · Hero · InteractiveViewer — props and events for each in the component docs.
# scaffold — Solid app + platform hosts + prebuilt native runtime $ npm create skal my-app # run it — emulator boots automatically if none is attached $ cd my-app && bun run dev:android # ship it — release APK with the bytecode cache baked in $ bun run build:android
The scaffold is a complete app: a Solid entry in src/App.jsx, platform hosts, hot reload wired, a Maestro smoke test, and the store ready to persist. Edit one file, save, watch the device.
Prereqs: bun and Flutter — the native runtime downloads prebuilt, so there's no LLVM, Rust, or NDK to install. Prefer a global command? npm i -g @skal/cli gives you skal dev / build / doctor.
One command scaffolds it. One more runs it on your phone. The third one ships it.