Perry replaces the JavaScript engine with LLVM, and publishes the benchmarks it loses
Project brief: A native TypeScript compiler written in Rust. Compiles TypeScript directly to executables using SWC and LLVM.
At a glance
- What is it?
- Perry is a TypeScript compiler written in Rust that parses with SWC and emits machine code through LLVM, so a TypeScript program ships as a single native binary for macOS, Windows, Linux, iOS, Android, watchOS and TV. Its own harness lists the cases where it is slower than Node and Bun, and it pins the report revision it trusts because the copy at HEAD reads zero.
- Who is it for?
- Perry suits a team shipping a command line tool, a server or a desktop app from TypeScript source who wants a small binary and real threads without maintaining a Node install per machine. It does not suit work that depends on the last few percent of Node compatibility or on class dispatch and tree traversal being fast.
- 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 Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- PerryTS/perry5,108 ★ · +710
| Repository | 2026-07-30 | 2026-11-14 |
|---|---|---|
| PerryTS/perry | 4.4k | 5.1k |
710 Stars in the last 107 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
One command replaces the engine, and the output has no runtime
The whole toolchain is a single invocation:
perry compile src/main.ts -o myapp
./myapp # a standalone native binary — ~34 KB for hello worldPerry parses your TypeScript with SWC and compiles it to machine code with LLVM, so what you ship is ahead-of-time output rather than something a JavaScript engine interprets or JITs at startup. That is the whole argument for the project: no Node install on every server, no embedded runtime inside a command line tool, no browser engine inside a desktop app. Because only the runtime your program actually uses gets linked, hello world lands near 34 KB and a real MongoDB GUI built with the compiler ships as roughly a 7 MB application.
The consequence worth planning around is that compilation happens before deployment instead of during it. Your start-up cost moves from the process launch to the build step, and any difference between your development machine and the target machine becomes a difference between two binaries rather than two runs of the same interpreter.
Eleven targets come out of one codebase, and web is the exception
The native targets cover macOS, Windows, Linux, iOS, Android, watchOS and TV, and the interface layer expands that list. A SwiftUI-like API compiles to real platform widgets rather than a WebView on macOS, iOS, iPadOS, visionOS, tvOS, watchOS, Android, Wear OS, Windows, Linux and Web/WASM, including home-screen widgets. The stated primitives underneath are AppKit, UIKit, Android Views, Win32 and GTK4.
One switch changes what you get. With `--target web` or `--target wasm` the compiler emits JavaScript or WebAssembly for the browser rather than a native binary, so the same source has two quite different deployment shapes and the table of native guarantees no longer applies to the web build.
That eleven target claim is the part to test against your own code. A UI that compiles to five platforms can still fail on the sixth for a widget the platform spells differently, and the examples directory shows demos rather than a compatibility matrix, with entries such as examples/widget_demo.ts and examples/windows_ui_demo.ts.
Shared mutable state does not compile, which is the point of parallelMap
Concurrency here is built into the compiler rather than bolted onto the runtime. `parallelMap`, `parallelFilter` and `spawn` run on real operating system threads, and the compiler rejects shared mutable state, so the claim in the README is that data races do not compile.
This is the sharpest difference from the JavaScript engines in the comparison table, where the equivalents are worker_threads, workers and processes. Here the compiler, rather than convention, is supposed to provide the guarantee.
The trade is that code which relies on mutation has to be rewritten rather than ported. A loop that accumulates into a shared array, or a cache that assumes a single writer, is exactly the shape that gets rejected, and the failure arrives at compile time instead of as a rare wrong answer at 3am.
Node compatibility is measured as a pass rate, not claimed as total
The compatibility claim is numeric: about a 97 percent pass rate on Node's own test suite across 53 `node:*` modules. The named implementations include `fs`, `http` and `http2`, `net` and `tls`, `crypto`, `stream`, `child_process`, `worker_threads`, `fetch` and the web globals, plus roughly fifty popular npm packages including Fastify, Express, mysql2, pg, ioredis, ws, bcrypt and jsonwebtoken. Plain JavaScript compiles too.
A pass rate is the honest way to state this, and it is also the number to plan against. Ninety-seven percent across fifty-three modules still leaves real gaps, and the repository is built around measuring them rather than asserting they are gone. There are scripts named run_module_parity.sh, run_parity_tests.sh and parity_status.sh, plus a test-compat directory, which is what you would expect from a project that treats module coverage as a tracked metric.
The practical consequence is that you find out which modules you need during the port, not after it. The npm packages on the list are the ones already checked, not a claim about the rest of the registry.
The published harness lists the rows Perry loses
The benchmark section is unusual in a way that matters for judging the project. Alongside the workloads Perry wins, the README publishes a generated table of the full suite including the rows it loses, cut from an artifact at a named commit, with lower wall-clock median better and medians within five percent of a peer counted as a tie.
The losses are specific. Class method dispatch costs 35 ms against 11 ms for Node and 8 ms for Bun. Binary tree allocation and traversal costs 13 ms against 6 ms for both. The Sieve of Eratosthenes costs 6 ms against 5 ms for both. String append and complex-number iteration are mixed, with Perry at 6 ms against 5 ms for Bun on the first and 25 ms against 24 ms for Node on the second. The wins are matrix multiplication and JSON roundtrip; the closure and modulo loops are ties.
So the honest summary is that Perry wins on matrix multiplication and JSON roundtrip in this suite and loses on class method dispatch, tree allocation and the sieve. If your program is a dispatch-heavy service rather than a batch job, read those rows before you believe the headline.
The report at HEAD reads zero, so the pinned revision is the one to trust
The benchmark provenance is the most unusual thing in the repository, and it is a warning rather than a boast.
The convolution and JSON figures are cited to a systems-language report pinned to commit 7beb3a50ca, described as the last revision whose artifact contains real values. The reason for the pin is stated plainly: the copy at HEAD was regenerated in pull request 7641 on a host where the harness clock was invalid, and it now reads 0.0 ms in every timing cell. The README says to re-pin to main once the file is regenerated. The Fibonacci, object allocation and array write figures come from a separate polyglot sweep in default mode with no fast-math.
That is a project telling you which of its own published numbers are unusable. It also means a reviewer cannot simply read the current file. The README's run-it-yourself command is ./benchmarks/run_public_baseline.sh, with the methodology in benchmarks/README.md.
Verify on your own hardware before quoting a figure. A benchmark broken by an invalid clock is exactly the failure mode that survives review when nobody reruns the harness.
Release plumbing is a dozen scripts, and the crates are split by target
The Cargo workspace shows how a compiler of this shape is arranged. There are more than fifty members, named by stage and target: perry-parser, perry-hir, perry-transform, perry-codegen and perry-dispatch for the front end, perry-abi, perry-runtime and perry-ffi for the boundary, and a separate crate per interface platform, including perry-ui-macos, perry-ui-ios, perry-ui-visionos, perry-ui-android, perry-ui-gtk4 and perry-ui-windows.
Native dependencies arrive as their own crates, among them perry-ext-bcrypt, perry-ext-argon2, perry-ext-better-sqlite3, perry-ext-sharp, perry-ext-ethers, perry-ext-ws and perry-ext-pdf, which matches the README's batteries-included claim covering databases and WebSockets.
Publishing is scripted through npm rather than by hand: publish:release, publish:nightly, publish:brew for a tap and publish:ffi for a cargo package, all under scripts/publish. A prepublishOnly hook runs an auth posture guard, and separate checks cover dependency freshness and pinned actions. Version numbers are dense, with v0.5.1664 published on 2026-11-01, so releases are frequent and the changelog is assembled under changelog.d.
Editorial conclusion
Perry suits a team shipping a command line tool, a server or a desktop app from TypeScript source who wants a small binary and real threads without maintaining a Node install per machine. It does not suit work that depends on the last few percent of Node compatibility or on class dispatch and tree traversal being fast. Before committing, run ./benchmarks/run_public_baseline.sh on your own hardware, read the pinned report rather than the copy at HEAD, and count the node modules and npm packages your code actually touches against the list of what is implemented.
Frequently asked questions
How does Perry compile TypeScript to a native binary?
SWC parses the TypeScript and LLVM compiles it to machine code, so the output is ahead-of-time code with no JavaScript engine at runtime. The command is perry compile src/main.ts -o myapp, and hello world lands at roughly 34 KB because only the runtime the program uses gets linked.
Does Perry support Node modules and npm packages?
About 97 percent of Node's own test suite passes across 53 node modules, including fs, http and http2, net and tls, crypto, stream, child_process, worker_threads and fetch, plus roughly fifty popular npm packages such as Fastify, Express, mysql2, pg, ioredis, ws and bcrypt. That is a measured pass rate, not complete coverage.
Which platforms can Perry build for?
Native binaries for macOS, Windows, Linux, iOS, Android, watchOS and TV, with a SwiftUI-like interface layer covering eleven targets including visionOS, Wear OS and GTK4 on Linux. With --target web or --target wasm it emits JavaScript or WebAssembly for the browser instead of a native binary.
Is Perry faster than Node.js and Bun?
It depends on the workload, and the project publishes the rows where it loses. It wins on matrix multiplication, JSON roundtrip, convolution and Fibonacci, and loses on class method dispatch at 35 ms against 11 ms for Node, binary trees at 13 ms against 6 ms, and the Sieve of Eratosthenes at 6 ms against 5 ms.
Can I trust the Perry benchmark numbers in the repository?
Not the copy at HEAD, by the project's own account. The convolution and JSON figures are pinned to commit 7beb3a50ca because the current file was regenerated in pull request 7641 on a host with an invalid harness clock and reads 0.0 ms in every timing cell. Rerun ./benchmarks/run_public_baseline.sh, with methodology in benchmarks/README.md.
How are Perry releases published?
Through npm scripts rather than by hand, covering a release pipeline, a nightly build, a Homebrew tap and an FFI package publish, all under scripts/publish, with a prepublishOnly auth posture guard. Versions are dense, with v0.5.1664 published on 2026-11-01, and the project is licensed MIT.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/perryts-perry)Star history chart for your README
[](https://hysenlabs.com/projects/perryts-perry)Was this useful?