QuickJS on a native runtime
Only your app’s code is JavaScript. The reactive graph, the renderer and every module you import are Rust; compiled JSX calls them directly.
- QuickJS
- nsh:rt
Write components in TSX. nsh runs them in QuickJS on a runtime written in Rust — reactive graph, renderer and layout included — and draws every element with UIKit or Android views. No web view, no Node.js, one binary.
nsh init my-app
nsh run my-app/App.tsxcounter.tsxOpen ↗The phone above runs this source. Your app’s code is the only JavaScript; everything it calls into is native.
signal()<view> · <text>style={{ … }}nsh runimport { mount, signal } from "nsh";
function Button(props: { label: string; color: string; onPress: () => void }) {
return (
<view
onPress={props.onPress}
style={{
backgroundColor: props.color,
borderRadius: 12,
paddingVertical: 12,
paddingHorizontal: 24,
alignItems: "center",
}}
>
<text style={{ color: "#ffffff", fontSize: 18, fontWeight: "bold" }}>{props.label}</text>
</view>
);
}
function App() {
const count = signal(0);
const history = signal<number[]>([]);
const change = (delta: number) => {
const next = count() + delta;
count.set(next);
history.update((h) => [next, ...h].slice(0, 5));
};
return (
<view style={{ flex: 1, backgroundColor: "#f4f5f7", padding: 24, paddingTop: 64, gap: 24 }}>
<text style={{ fontSize: 28, fontWeight: "bold", color: "#1f2328" }}>nsh counter</text>
<view
style={{
backgroundColor: "#ffffff",
borderRadius: 16,
padding: 24,
alignItems: "center",
gap: 8,
}}
>
<text style={{ fontSize: 14, color: "#6e7781" }}>Current value</text>
<text style={{ fontSize: 64, fontWeight: "bold", color: count() < 0 ? "#cf222e" : "#1f2328" }}>
{count()}
</text>
</view>
<view style={{ flexDirection: "row", gap: 12, justifyContent: "center" }}>
<Button label="−1" color="#cf222e" onPress={() => change(-1)} />
<Button label="+1" color="#1f883d" onPress={() => change(1)} />
</view>
<view style={{ gap: 6 }}>
<text style={{ fontSize: 14, color: "#6e7781" }}>
Recent values: <text style={{ fontWeight: "bold", color: "#1f2328" }}>{history().length}</text>
</text>
<each of={history()}>{(value) => <text style={{ fontSize: 16, color: "#1f2328" }}>• {value()}</text>}</each>
</view>
</view>
);
}
mount(App);Each of these is something you can check in the repository: the crate, element or command is named on the card.
Only your app’s code is JavaScript. The reactive graph, the renderer and every module you import are Rust; compiled JSX calls them directly.
signal, computed and effect: fine-grained reactivity scheduled natively. A change re-renders only what reads it.
Taffy lays out every node on the engine thread, with React Native’s style semantics plus CSS grid.
class="p-4 rounded-xl" becomes styles in Rust; unknown classes are reported at build time, and themes live in nsh.json.
Every element is a UIView or android.view.View, so maps, web views and video embed with no layering tricks.
Timing, spring and decay drivers run in a native animation graph. JS starts them and is told when they settle.
Recycled cells, columns, sticky section headers and inverted chat lists, with cells bound natively while you scroll.
nsh bundles with Rolldown and Oxc, writes type declarations, serves dev builds and packages apps. No node_modules.
Save a file and only the edited components render again; the rest of the app keeps its state. Logs stream to your terminal.
Ship updates as .nshpkg: Ed25519-signed, release keys certified by an offline root, revocation lists checked on device.
Put an AppView or NshView into an existing iOS or Android app — several instances, no global singletons — and register your own native components.
Button, Card, Sheet and the rest of nsh/ui/mobile are Rust, run in the core and look the same on iOS and Android.
Everything one JS turn changes is queued natively, applied in a single Core::commit, and handed to the main thread as one MountBatch. The main thread never waits for JS.
Callbacks the system needs answered on the spot — may this text change, may the back button pop — are answered natively from props mounted beforehand; JS hears about it afterwards.
No per-item FFI calls. Node ids are dense indexes, and unchanged styles and frames are never sent.
CoreText or StaticLayout measures and lays out text on the engine thread; the main thread only draws the result.
Install the toolchain, write a first app, and learn how state, layout, styles and navigation work.
Read02The host elements every UI is built from — view, text, image, scroll views, lists — and their props.
Read03nsh/ui/mobile: buttons, cards, sheets and the rest, written in Rust and identical on iOS and Android.
Read04The modules apps import: nsh, nsh/animated, navigation, storage, the platform modules and the web APIs.
Read