# pocketjs: one bundle, one thread, and a benchmark table that records its own losses

> PocketJS runs Solid, Vue Vapor or Octane TypeScript components in a QuickJS guest while a Rust core does flexbox layout and draws every pixel, so there is no DOM, no CSS engine and no WebView anywhere in the pipeline. It ships for a PSP at 333 MHz, a Vita, a 3DS and a list of older phones, and the project publishes the numbers where it loses as carefully as the ones where it wins.

**pocket-stack/pocketjs** — PocketJS is a portable application runtime that turns modern component code into native pixels across radically different hardware.

- Repository: https://github.com/pocket-stack/pocketjs
- Website: https://pocketjs.dev
- Stars: 1,720 · Forks: 101
- Language: TypeScript
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/pocket-stack-pocketjs

## Two homepages, two npm packages, and a command called pocket

The project's own metadata disagrees with itself about where it lives. The repository field in package.json points at a different owner than the repository it ships in, naming pocket-nexus where the repository is pocket-stack. And there are two homepages: the homepage recorded for the repository is pocketjs.dev, while the homepage field in package.json is pocketjs.pocket.nexus, and every link on the page points at the second one, including the website, the playground, the documentation, the blog and the changelog.

So a visitor who trusts the repository badge lands on one domain and a reader who follows the page's own links lands on another. Neither is a fatal problem, but it is the kind of thing that makes a project's age visible, and it is worth checking which domain is the live one before you file anything.

The packaging names are a third layer. Two npm packages are advertised in the header, one for the framework and one for the CLI, under one scope. The command those packages give you is `pocket`, not `pocketjs`, which is the name that appears in every example on the page:

```sh
pocket create my-app
pocket check --target psp     # ok  480x272 · text.glyphs.baked · input.buttons
pocket check --target vita    # ok  same bundle, density 2, no component edited
```

The JavaScript side is pinned with `bun.lock` and no package-lock, and the desktop benchmark is reproduced with `bun tools/bench-desktop.ts`. So the toolchain is Bun, and the entry point is a shorter name than the project.

## The published tarball is a hand maintained allowlist

The `files` array in package.json is the whole publishing contract for this project, and it is written out by hand rather than derived. It names framework source and compiler paths, three contracts directories for schema, spec and generated code, the tools directory with one negation to keep the desktop bench out, then individual files inside the hero demo rather than the directory.

Then the device work, listed one host at a time: an iOS NativeScript host, a legacy iOS host, then iphone2g, iphone4s, ipodtouch6, meizu-m8, blackberry-classic, blackberry-classic-qnx, web and android. Each has a demo app and a markdown file of its own, with names like MEIZU_M8.md and VITA-USB.md, and the BlackBerry target appears twice because it has two backend classes.

The asset list is where the maintenance cost shows. The fonts directory and the brand directory are named as whole directories, but the logo and eight separate spinner frames are listed individually, spinner-00.svg through spinner-07.svg. Every one of those lines is a decision someone has to remember to make again for the next asset, and the failure mode is quiet: a path that is not on the list is simply absent from the published package, with no error and no test to catch it.

The single negation in the list, the exclusion of the desktop bench from tools, shows the list is used as an exclusion mechanism too, so a future maintainer can keep a heavy local tool from shipping. That is the right use of the mechanism. The cost is that the list itself is unenforced, and it has to be read as a changelog of what someone remembered.

## The desktop table names a loss and then two more in prose

The comparison that matters most to a sceptical reader is the desktop one, and it is the one where the project is most careful. The setup is stated first: historical August 2026 results, for the previous gpui host, the same markdown editor built three ways on an Apple M3 Max, with the full report in the repository and a one line command that reproduces it.

| | pocket | Tauri v2 | Electron |
| --- | --- | --- | --- |
| Processes | 1 | 4 | 5 |
| Cold start to first painted frame | 149 ms | 380 ms | 301 ms |
| Idle resident memory | 83 MB | 193 MB | 382 MB |
| On disk | 10 MB | 9 MB | 242 MB |

Three wins and one loss, and the loss is in the row most people skim. On disk, Tauri v2 is a megabyte smaller than the pocket build, at 9 MB against 10 MB, while the same table shows the pocket build winning cold start by more than twice Electron's figure and cutting idle memory to a fifth of Electron's. A table with a loss in it is worth more than a table without one.

Then the prose adds the two numbers the table cannot hold. With a document open and no input, the pocket build redraws about twice a second, for the blinking caret, so the idle CPU figure is not idle. And the report records where the pocket build loses on a longer document, because the editor re-wraps the whole document through the QuickJS interpreter on every keystroke, which is why its storm CPU rises with document length. The cost is architectural, not accidental: text layout of a changing document is work the runtime sends to the guest, and a per keystroke full pass has to show up somewhere.

## The two hero demo rows differ by four times, and the page explains why

The handheld numbers are tighter, and they come with the caveat that is usually missing. The hardware is one MIPS core at 333 MHz with 32 MB of RAM, measured against the 16.67 ms frame budget that 60 fps implies. The OpenStrike frame budget result is 2.2 ms of JavaScript and 8.4 ms of total CPU work, with a worst observed frame of 9.7 ms. Seven samples per application.

Then two rows that look like they should be comparable and are not:

| Measurement | Result |
| --- | --- |
| Hero demo, cost of a virtual DOM | Solid 15.15 ms · Vue Vapor 16.74 ms · Vue with a virtual DOM 90.75 ms |
| Hero demo, the three shipped frameworks | Solid 3.66 ms · Vue Vapor 3.61 ms · Octane 6.53 ms |

Solid appears in both rows, at 15.15 ms and then at 3.66 ms, a difference of more than four times for the same framework on the same demo. The page gives the reason in one sentence: the two rows come from separate runs with different toolchain versions, so each row is comparable internally but not against the other. That sentence is the most valuable sentence in the benchmarks, because it tells you the number you would have compared across rows is meaningless.

The virtual DOM row also settles a question the other row leaves open. A Vue build with a virtual DOM costs 90.75 ms on a demo where the framework this project ships costs 3.61 ms, which is the actual argument for compiling to a native tree instead of shipping a reconciler. Note that Octane appears only in the second row, so the framework that has no virtual DOM at all is absent from the comparison that measures virtual DOM cost.

## The footprint claim is a memory share and a clock ratio, and it adds up

The small footprint section makes one claim with three comparisons attached: a complete application drawing an animated interface occupies 8 MB on a single 333 MHz core, which is a quarter of the PSP's 32 MB, one part in 1536 of a 12 GB iPhone 17 Pro Max, on a core clocked 13 times slower than an A19 Pro performance core at 4.26 GHz.

The arithmetic holds. Twelve gigabytes is 12288 megabytes, and 12288 divided by 8 is exactly 1536. And 4.26 GHz against 333 MHz is a factor of 12.8, which rounds to the 13 claimed. So the numbers are internally consistent, which is more than most footprint claims manage.

What they are, though, matters as much as that they check out. This is a memory share comparison against a phone and a clock ratio against a phone core, not a capability comparison. Nothing here says the PSP draws as much as an iPhone, and the same page is careful elsewhere to keep the two apart: the iPhone appears only as a denominator. The one measurement that is a real frame budget is the 16.67 ms one, with a worst observed frame of 9.7 ms, and that leaves headroom of about 7 ms per frame on a core with no browser engine competing for it.

## No CSS engine means a fixed Tailwind subset and no way to compose at runtime

The styling model is the clearest expression of what this runtime gives up. Class literals are compiled into a baked style table at build time, and the runtime resolves a class attribute by lookup. The page is explicit about what that removes: no CSS parser, no cascade, no specificity resolution and no reflow on the device. The accepted vocabulary is a fixed Tailwind subset, and the styling documentation is where you would enumerate it.

What you get in exchange is predictability. Nothing on the device can change a style, because there is no cascade for a later rule to win and no reflow pass to trigger. The component API follows the same logic: host props rather than DOM events, with a press handler and a focusable flag in the example instead of click and tabindex, and focus state expressed as a variant in the class list.

The cost shows up in two places in the visible page. The Counter sample, which starts from a Solid signal and a two element view, ends mid-attribute on its second text element, so the most complete example on the page is an incomplete one. And the fixed subset means any styling you have already written in ordinary CSS, with custom properties, with media queries, with a stylesheet you load at runtime, has no path into this runtime at all. That is a real cost, and it is the price of the 8 MB figure rather than something the project presents as free.

## Animation runs on the core clock, and text layout runs on the interpreter

Two claims about JavaScript execution sit on the same page, and both are true because they are about different work. Keyframe timelines and spring curves are baked into the same style table at build time and advanced by the Rust core on its own clock, so a screen animates with no per frame JavaScript. Motion Lab runs the yui540 studies, the standard CSS animation stress cases, compiled to WebAssembly on the website inside an interactive model of a PSP, and again on the handheld those studies were written for.

That is a strong result and a narrow one. Timeline playback never touches the guest, because the curves were already resolved at build time into a table the core can step. Text layout of a document that is changing is not in that category, and the desktop report says so plainly: the editor re-wraps the whole document through the QuickJS interpreter on every keystroke, so storm CPU rises with document length.

The architecture diagram in the page draws the same line from the other direction. It compares one thread and one process against four threads and two processes, listing component, framework runtime and virtual DOM diff, DOM mutation, CSSOM with cascade and specificity, style recalculation, and then layout. The last row of that column carries no thread label at all, under a header that says four threads.

So the honest summary of the execution model is that anything precomputed is free at runtime, and anything that depends on the shape of changing data is paid for in the guest, per frame, in an interpreter.

## The target gate refuses to build for a device that does not match the declaration

The mechanism that makes one bundle serve wildly different hardware is a gate, and the page shows what it prints. An application declares its viewport and its required APIs in `pocket.json`, and a target profile has to satisfy that declaration before compilation and packaging proceed. For the PSP the output names a 480 by 272 viewport with baked text glyphs and button input. For the Vita the same bundle passes at density 2, with no component edited.

Underneath, a per target backend submits the draw list, and the page names the set: GE and GXM for the PlayStation targets, Metal for Apple hardware, wgpu, software rasterization, and e-ink updates. The host directories in the published manifest match that list, including the pair of BlackBerry hosts that exist because the device has two backend classes, and the v0.11.0 release notes call the second of them a second backend class that paints through gpui.

The same release line adds the compilation path: v0.13.0 ships MicroTS, which compiles Solid and Vue TypeScript applications to native Rust, and a wired development runtime for the PS Vita. So there are two ways to get to the native tree, an interpreter guest and an ahead of time compiler, and MicroTS is the one that removes the interpreter from the loop.

One thing the page does not do is finish. The contents list promises thirteen sections, including Replay every frame, Choose your screen, Get started and Ahead-of-time compilation, and the text runs out inside the desktop numbers, on a line holding only two letters. That is where the setup instructions would have been, which is why the page's own examples remain the only place the commands appear.

## Conclusion

Read this as a research runtime with a shipping device list, not as a drop in replacement for a browser stack, and the page supports that reading well. For a hobby project, a game, or an embedded panel on hardware nobody makes any more, the combination of a baked style table, host input props and a pre-compilation target gate is a coherent design, and the PSP numbers are specific enough to check. Four things to settle before you commit. First, there is no install instruction in the visible page: the two npm packages are badges, the command is `pocket`, and the setup section named in the contents is never reached on the page as it stands, so you are choosing your own entry point. Second, the styling vocabulary is a fixed Tailwind subset with no cascade, which is the price paid for having no CSS engine, and any design you have already written in real CSS will not transfer. Third, the desktop figures describe a previous host and an August 2026 build, and the same table shows another runtime shipping a megabyte smaller on disk, so treat them as a dated report rather than a standing claim. Fourth, the desktop report says the editor re-wraps the whole document through the JavaScript interpreter on every keystroke, which is the kind of cost that decides whether the runtime suits your workload. What the project does well is publish the losses, name the toolchain difference between its own two benchmark rows, and keep the state of a mission in files you can read without the tool.

## FAQ

### What is PocketJS?

A portable application runtime. Applications are written in TypeScript with Solid, Vue Vapor or Octane components, a QuickJS guest runs them, and a Rust core performs flexbox layout and draws every pixel in one thread inside one process, with no DOM, no CSS engine and no WebView.

### Which devices does PocketJS target?

The published manifest names hosts for iPhone 2G, iPhone 4S, iPod touch 6, Meizu M8, BlackBerry Classic in two backend classes, Web and Android, plus iOS through NativeScript and a legacy iOS host. The backends named for drawing are GE, GXM, Metal, wgpu, software rasterization and e-ink.

### How do I check whether a PocketJS app will run on a target?

Declare the viewport and the required APIs in `pocket.json`, then run `pocket check --target <name>`. A target profile that does not satisfy the declaration stops compilation and packaging before they begin.

### What does MicroTS do in PocketJS?

It is an ahead of time compiler that turns Solid and Vue TypeScript applications into native Rust. The v0.13.0 notes announce it alongside a wired development runtime for the PS Vita.

### Does PocketJS allow dynamic styling at runtime?

Not in the CSS sense. Class literals are compiled into a baked style table at build time and resolved by lookup, with a fixed Tailwind subset as the vocabulary, so there is no parser, cascade, specificity resolution or reflow on the device.

## Sources

- [License: MIT](https://github.com/pocket-stack/pocketjs/blob/main/LICENSE)
- [pocket-stack/pocketjs on GitHub](https://github.com/pocket-stack/pocketjs)
- [Project website](https://pocketjs.dev)
- [README](https://github.com/pocket-stack/pocketjs/blob/main/README.md)
- [Releases](https://github.com/pocket-stack/pocketjs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pocket-stack-pocketjs
