PocketJS: a QuickJS and Rust runtime that bakes JSX into pixels on a PSP
PocketJS is a portable application runtime that turns modern component code into native pixels across radically different hardware.
At a glance
- What is it?
- PocketJS compiles Solid, Vue Vapor and Octane components into one native tree driven by a QuickJS guest and a Rust core. It is aimed at hardware that has no browser, and the README is explicit about what it gives up to get there.
- Who is it for?
- PocketJS fits teams shipping component-based UI to handhelds and legacy phones where a browser stack will not run, and who accept a fixed Tailwind subset and a baked style table in exchange for one bundle that serves several densities. It is the wrong choice if you need the full CSS cascade, arbitrary runtime styling, or a DOM you can query.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 18, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem PocketJS is aimed at
Put a modern component framework on a PlayStation Portable and the first thing that fails is not your code. It is everything underneath it. The README's own execution diagram spells the difference out: a browser or WebView path spreads work across four threads and two processes, from framework runtime and virtual DOM diffing through CSSOM, cascade, specificity, style recalculation, layout, reflow, paint records, a layer tree, a tiling compositor, a raster pool, an IPC hop to a GPU process and sync fences before anything reaches the screen. PocketJS replaces that with one thread and one process: your component, a renderer adapter, the native tree, flexbox layout against a baked style table, a draw list, and a backend draw call.
The intended audience is narrow and identifiable. It is people writing application UI for devices that either never had a browser or cannot afford one: the PSP and Vita in the README's own examples, plus the phone hosts that ship inside the npm package (iphone2g, iphone4s, ipodtouch6, meizu-m8, blackberry-classic and their variants). If you are building for a modern phone or a desktop, this runtime is solving a problem you do not have, and it will cost you the CSS features you currently rely on.
One native tree, three frameworks, no DOM
The architecture has a clean split. The guest is a QuickJS instance that runs your component code. The core is Rust, and it owns flexbox layout, the baked style table, and the draw list. A per-target backend submits that draw list through one of several declared hosts: the README lists GE, GXM, Metal, wgpu, software rasterization and e-ink updates. Nothing in that chain parses CSS or maintains a DOM.
Framework primitives are imported directly from solid-js, vue or octane, and the README states that the framework choice changes application code and nothing below it. All three compile to the same native tree on the same QuickJS guest. Vue Vapor accepts both JSX and script setup single-file components; Octane is described as compiled hooks and JSX with no virtual DOM.
Styling is where the design gets opinionated. Class literals are compiled into a baked style table at build time, and the runtime resolves a class attribute by lookup. That means no CSS parser, no cascade, no specificity resolution and no reflow on the device. The accepted vocabulary is a fixed Tailwind subset, enumerated in the project's styling documentation. Animation follows the same pattern: keyframe timelines and spring curves are baked into the same table and advanced by the Rust core on its own clock, so a screen can animate with no per-frame JavaScript.
Installing PocketJS and running a first check
The README points at npm packages @pocketjs/framework and @pocketjs/cli, and the repository's package.json names the framework package @pocketjs/framework at version 0.12.0 under MIT. The README does not print a single install command line, so the package names are the traceable part; the CLI binary appears in the README only as pocket check.
A component in PocketJS looks like ordinary Solid. This example is adapted from the counter in the README, which imports createSignal and Show from solid-js, mount from @pocketjs/framework/solid, and Text and View from @pocketjs/framework/solid/components:
import { createSignal, Show } from "solid-js";
import { mount } from "@pocketjs/framework/solid";
import { Text, View } from "@pocketjs/framework/solid/components";
function Counter() {
const [count, setCount] = createSignal(0);
return (
<View class="w-full h-full flex-col items-center gap-4 p-4 bg-slate-50">
<Text class="text-xl text-slate-950 font-bold">Count: {count()}</Text>
</View>
);
}
mount(() => <Counter />);The class strings are not interpreted at runtime. They are compiled into the baked style table, so anything outside the documented Tailwind subset will not resolve.
The first real step before packaging is target admission. An application declares its viewport and required APIs in pocket.json, and a target profile has to satisfy that declaration before compilation and packaging proceed. From the application directory, the README gives these two commands:
pocket check --target psp # ok 480x272 · text.glyphs.baked · input.buttons
pocket check --target vita # ok same bundle, density 2, no component editedThe comments in that block are the README's own, and they are the useful part: the same bundle passes on a 480x272 PSP profile and on a Vita profile at density 2 without a component being edited. If pocket check fails, the target profile does not meet what your pocket.json declared, and compilation and packaging will not proceed.
The frame contract changes how tests behave
PocketJS makes the frame counter the clock. One frame(buttons) call is a transaction that nothing outside it can interrupt, and the README states that nothing waits on a wall clock. The stated consequence is that tests run as fast as the CPU allows without changing the timing they measure: a journey that takes six seconds in front of a user is a few dozen frames in CI.
The rules around that transaction are strict. Effects land on frame boundaries, so a network reply arriving partway through frame +3 is queued rather than applied, and is delivered at the start of frame +4 in FIFO order before any application hook runs. The README says this removes microtask races and mid-frame callbacks. The after() function replaces setTimeout with a deadline measured in frames.
This is a real design commitment, and it is the kind of thing that will surprise a developer coming from a browser. Code that assumes a timer fires at a wall-clock moment will not port cleanly. Code that assumes a promise resolves between frames will need rethinking. The payoff is determinism: the same input sequence produces the same frame sequence regardless of how fast the machine is.
Where PocketJS is the wrong tool
The baked style table is the sharpest limitation, and it is structural rather than temporary. Because class literals are compiled at build time, a class string that is not in the fixed Tailwind subset simply has no entry to resolve. There is no cascade, so you cannot layer a stylesheet over a component and expect precedence rules to sort it out. Applications that compute class names at runtime, that ship user-authored themes, or that depend on inherited and overridden properties are outside what this runtime does.
There is also no DOM. Any code that reaches for document, that queries nodes, or that relies on a library inspecting the tree will not work. That is stated plainly in the README as a design property, not a gap to be filled later.
Target admission cuts the other way too. A bundle is compiled and packaged against a target profile that must satisfy your pocket.json declaration. That is what makes one bundle serve several densities, but it also means the declaration is a contract you have to maintain. Add a required API and targets that cannot provide it stop passing pocket check.
The maintenance picture is current: the last push was on 2026-09-17, the repository is not archived, and v0.12.0 shipped the same day. That is a fast-moving pre-1.0 project. The npm package is at 0.12.0, and the README documents no rollback path for a bad release.
How this differs from shipping a WebView shell
The obvious alternative for putting component code on unusual hardware is a WebView shell: keep the browser engine, wrap it in a native host, and let the existing CSS and DOM stack do the work. The difference in approach is the whole point of PocketJS. A WebView keeps the CSS engine, the cascade and the compositor, and pays for them in threads, processes and memory. PocketJS deletes that layer and replaces it with a build-time style table plus a Rust layout and draw path.
That trade is not free in either direction. A WebView shell runs code you already have, with the CSS you already wrote, and it runs on any device that has a usable engine. PocketJS runs only on targets with a declared backend, and only with the fixed style vocabulary. What you get in return is the single-thread, single-process execution model the README diagrams, plus frame-boundary effect delivery and animation that runs without per-frame JavaScript.
For a device like the PSP, the choice is not really between two equivalents. A WebView is not an option there, so the realistic baseline is writing native UI by hand in C or C++, or not shipping the application at all.
Licence and what a version bump costs
PocketJS is MIT licensed, and package.json carries the MIT identifier for @pocketjs/framework. That is permissive: you can use it in commercial and closed-source applications, and you keep your own source. This is not legal advice, and the obligations you actually carry depend on the full text in LICENSE and on the licences of everything the runtime links against, including QuickJS and the Rust crates in engine/Cargo.toml and engine/Cargo.lock. Read those before you ship a binary.
Upgrade cost is harder to estimate from the README. The version is 0.12.0 and the project is pre-1.0, so minor bumps can carry behaviour changes. The v0.11.0 notes describe two more device families running PocketJS, a second backend class painting through gpui with host-side text measurement, and a paired Nintendo 3DS taking new guests over Wi-Fi. v0.12.0 covers whole-text CJK loading, background resources and device-aware presentations. Those are feature-sized changes landing in minor versions. The README does not document a rollback procedure, so pinning the version in your lockfile is the only stated protection.
Editorial conclusion
PocketJS fits teams shipping component-based UI to handhelds and legacy phones where a browser stack will not run, and who accept a fixed Tailwind subset and a baked style table in exchange for one bundle that serves several densities. It is the wrong choice if you need the full CSS cascade, arbitrary runtime styling, or a DOM you can query. Before committing, run pocket check against each target you intend to ship and confirm the target profile satisfies the viewport and required APIs your pocket.json declares.
Frequently asked questions
Can I make apps with JS using PocketJS?
Yes. PocketJS runs a QuickJS guest that executes your component code, and the README shows Solid, Vue Vapor and Octane components compiling to one native tree. The JavaScript runs on the guest, while layout and drawing happen in the Rust core.
What is PocketJS?
It is a portable application runtime that turns modern component code into native pixels across different hardware. A QuickJS guest drives a Rust core that performs flexbox layout and draws every pixel in one thread inside one process, with no DOM, no CSS engine and no WebView.
How do I style a PocketJS component?
You write Tailwind class literals on components such as View and Text, and the build compiles those literals into a baked style table. The runtime resolves a class attribute by lookup, so the accepted vocabulary is a fixed Tailwind subset enumerated in the project's styling documentation.
How do I check whether a PocketJS bundle runs on a specific device?
The application declares its viewport and required APIs in pocket.json, and you run pocket check with a target such as psp or vita from the application directory. Compilation and packaging proceed only if the target profile satisfies that declaration.
Does PocketJS use a DOM or a CSS engine?
No. The README states that PocketJS has no DOM, no CSS engine and no WebView. The guest emits tree mutations, and the Rust core owns layout, the style table and the draw list.
Community notes