# PrimJS is a JavaScript engine shaped around one framework

> PrimJS is an Apache 2.0 JavaScript engine built on QuickJS for the Lynx cross-platform framework, with a template interpreter that uses stack caching and register optimisation, a tracing garbage collector in place of QuickJS reference counting, Chrome DevTools Protocol debugging and initial WebAssembly support. The catch is that its most interesting features are only enabled by default on arm64 targets, which excludes Linux and x86 builds.

**lynx-family/primjs** — JavaScript Engine Optimized for Lynx

- Repository: https://github.com/lynx-family/primjs
- Website: https://lynxjs.org
- Stars: 1,155 · Forks: 64
- Language: C++
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/lynx-family-primjs

## An engine with a customer

PrimJS is described as a lightweight, high-performance JavaScript engine designed specifically for the Lynx cross-platform framework, fully supporting ES2019, and built on top of QuickJS. The README says it delivers superior performance and a better development experience compared to QuickJS.

Every one of those clauses narrows the scope, and together they define the project precisely. It is not a general purpose engine competing with V8 or SpiderMonkey. It is an engine for one framework, which means its object model, its memory strategy and its tooling decisions can all be made with that framework's workloads in mind rather than the whole web's.

That is a normal and healthy way for an engine to be built. The QuickJS base is a well known small engine, and taking it as a starting point rather than writing a parser and a bytecode interpreter from scratch is the single biggest decision in the project. What PrimJS adds is the part that QuickJS was never going to build for you: an object model that speaks to Lynx, a garbage collector rather than reference counting, and a debugger that a developer can attach to.

The audience is therefore two groups. The first is the Lynx development team itself, for whom this is the engine their framework runs on. The second is the broader open source community of people who want a small engine with these particular properties, and who are willing to accept that some of the improvements only apply on particular architectures.

The licence is Apache 2.0, and the repository is not archived, with the last push on 2026-10-19 and releases 4.1.1 in August 2026 and 4.0.0 in July 2026.

## Three departures from QuickJS: interpreter, collector, object model

The feature list is the useful part of the README, because it says precisely what PrimJS changes rather than that it is fast.

The first change is the interpreter. The optimized interpreter is documented as introducing a template interpreter that leverages stack caching and register optimizations, and the README says it significantly enhances performance. A template interpreter is a design in which the hot path of the bytecode is shaped into a fixed sequence rather than dispatched generically, and the register optimisation is the same idea applied to how values are held. Both are changes to the execution core, which is the part of an engine that is hardest to change safely.

The second change is the object model. The engine's object model integrates efficiently with the Lynx object model, reducing data communication overhead and improving rendering performance. That is the part that makes this an engine for a framework rather than a general one, because a Lynx element is not a DOM node, and the bridge between the script world and the rendering world is where the cost lives.

The third change is the collector, described in detail in the next section.

On top of those there are two capabilities that are about working with the engine rather than about running code faster. The debugging support is a full implementation of the Chrome DevTools Protocol, which is what makes it possible to attach Chrome Debugger and step through a Lynx application. The WebAssembly support is described as initial, covering module loading, instantiation and basic JavaScript-to-WASM interoperability.

The performance claim is stated narrowly and is worth reading in that form: the benchmark results show that PrimJS outperforms QuickJS by approximately 28% in overall score, 3735 versus 2904, on the Octane Benchmark suite, with a link to a performance comparison document. It is a claim about one suite, and the README is explicit that the team is continuously working on performance optimizations.

## A tracing collector instead of reference counting

The memory management section is the most consequential design change, and it is the one that changes the shape of the code that runs on top of the engine.

PrimJS uses a garbage collector instead of QuickJS's reference counting. The README states three benefits: better performance, improved memory analyzability, and reduced risk of memory leaks.

The mechanism behind those three is the same mechanism. Reference counting frees an object when its last reference goes away, which means the lifetime of an object is visible in the code as a count, and a cycle of objects that reference each other is never collected unless the engine adds cycle detection. A tracing collector, which follows references from roots and collects whatever it cannot reach, handles cycles by construction and makes the cost of garbage collection a matter of tracing rather than a matter of counting.

The performance argument is less obvious and more interesting. Under reference counting, every reference assignment costs work at the point of assignment, which is spread across the whole program. Under tracing, most of that work disappears from the hot path and reappears occasionally as a collection pause. For an engine whose interpreter was just optimised with stack caching and register allocation, moving the memory cost out of the hot path is a coherent strategy, and it is consistent with the PrimJS snapshot, compatible memory management and tracing GC being treated as a set.

The memory analyzability claim is a tooling claim, and it follows from the same design. With reference counting, the answer to why an object is still alive is a count spread across the program. With a tracing collector, the answer is a path from a root, which is something a profiler or a heap snapshot can show directly.

## The interesting features only switch on for arm64

This is the constraint that catches people, and the README states it in a paragraph that is easy to skim past.

PrimJS snapshot, compatible memory management, and tracing GC are enabled by default only for arm64 targets with pre-generated embedded.S files. Currently these targets are Android, iOS and macOS. The features remain disabled on Linux, on other platforms without embedded.S, and on non-arm64 targets.

So the garbage collector, the snapshot mechanism and the compatible memory management are arm64 features today, and on x86 or on Linux you get a different engine from the one the performance work was aimed at. The interpreter optimisation and the object model integration are not under the same restriction, but the memory work is, and the memory work is where the largest claims sit.

You can target arm64 explicitly, and on macOS this also enables these features by default:

```bash
gn gen out/Default --args='target_cpu="arm64"'
ninja -C out/Default -j32 qjs_exe
```

A release build is configured with the is_debug argument set to false.

For a user, the practical reading is that the engine's headline capabilities line up with the platform Lynx actually ships on, which are mobile platforms, so the restriction is not arbitrary. It does mean that a developer benchmarking PrimJS on a Linux workstation is measuring a different configuration from the one that runs on a phone, and the README's benchmark numbers do not say which configuration they came from.

## Building and running: Habitat, gn, ninja and qjs

The build has three stages and the toolchain is unusual enough to be worth reading carefully.

First the checkout and the dependency sync, which uses Habitat:

```bash
git clone git@github.com:lynx-family/primjs.git
cd primjs
source tools/envsetup.sh
hab sync .
```

The envsetup script is what puts the Habitat tools on the path, and hab sync . fetches the dependencies that the build declares.

Then the build itself, which uses gn and ninja to generate the qjs binary:

```bash
gn gen out/Default
ninja -C out/Default qjs_exe
```

And then the run. The primary binary is qjs, located at out/Default/qjs, and it runs JavaScript files:

```bash
./out/Default/qjs test.js
```

The tests are driven by two Python scripts from the root directory, one to build the unit tests and one to run them:

```bash
python3 tools/ci/check_test_build.py
python3 tools/ci/check_test_run.py
```

The repository layout explains the rest. There is a .gn directory and a BUILD.gn, which is the Chromium style build system, a DEPS file and a patches/ directory, which is the same dependency management style, a .habitat directory for the Habitat plan, a CMakeLists.txt for the alternate build, a PrimJS.podspec for the iOS side, a harmony/ directory for the OpenHarmony port, third_party/ and packages/ for vendored code, a PRIMJS_VERSION file, and a Gemfile for the Ruby tooling around the third party directories.

The licensing files at the root are worth a glance too, since there are LICENSE, LICENSE.nodejs and LICENSE.v8 alongside NOTICE. The engine carries parts of two other codebases, and the notices are how a user tells which terms apply to which part.

## Debugging, WebAssembly, and the shape of the project

The debugging support is the feature that changes how a team works with the engine day to day. A full implementation of the Chrome DevTools Protocol enables integration with Chrome Debugger, which means the developer tools people already know work against a Lynx application, rather than a bespoke debugging interface that has to be learned and maintained.

The WebAssembly support is more modest and the README is careful to say so, describing it as initial support for module loading, instantiation and basic JavaScript-to-WASM interoperability. Those three capabilities are the ones a JavaScript engine needs before it can host WebAssembly at all, so the description is honest about where the work is.

The project is run as part of the Lynx organisation rather than as a standalone project with its own community. The contributing guide points at the Lynx repository, the call to join is framed as joining the Lynx Authors, and large discussions and proposals are handled in GitHub Discussions on the PrimJS repository itself.

The default branch is develop, and the release line moved quickly through 2026, with 4.0.0 in July, 4.1.1 in August, and a separate weak-node-api-v0.0.6 tag in May that suggests an experimental line is maintained alongside the main one.

For an adopter the practical questions are narrow and answerable from the repository. Does your target platform enable the features you want? Does the ES2019 support cover your application's syntax? And what do the V8 and Node derived licence files oblige you to do if you distribute the engine.

## Conclusion

PrimJS suits a team working on the Lynx framework, or a developer who wants a small engine with a modern garbage collector, Chrome DevTools debugging and an interpreter optimised for template-shaped code rather than for general browsing workloads. It does not suit a project that wants a general purpose JavaScript engine, since the object model integration is written for Lynx, and it is the wrong choice if you need the optimised interpreter, snapshot and tracing GC on x86 or on Linux, since the README says those features stay disabled there without pre-generated embedded.S files. Verify first which target you are building for, whether the features you want are enabled in that configuration, and whether the QuickJS base and the V8 and Node derived parts carry obligations that matter for your distribution.

## FAQ

### What is PrimJS?

It is a lightweight, high-performance JavaScript engine designed specifically for the Lynx cross-platform framework, fully supporting ES2019 and built on top of QuickJS, with a template interpreter, a garbage collector rather than reference counting, Chrome DevTools Protocol debugging and initial WebAssembly support.

### How is PrimJS different from QuickJS?

It replaces QuickJS's reference counting with a garbage collector, adds a template interpreter using stack caching and register optimisations, integrates its object model with the Lynx object model to reduce data communication overhead, and implements the Chrome DevTools Protocol for debugging. The README reports roughly 28% better overall score than QuickJS on the Octane Benchmark suite.

### On which platforms are the snapshot and tracing GC features enabled?

PrimJS snapshot, compatible memory management and tracing GC are enabled by default only for arm64 targets with pre-generated embedded.S files, which are currently Android, iOS and macOS. They remain disabled on Linux, on other platforms without embedded.S, and on non-arm64 targets.

### How do I build PrimJS and run a script with it?

Clone the repository, run source tools/envsetup.sh and hab sync . to pull dependencies, then build with gn gen out/Default and ninja -C out/Default qjs_exe. The primary binary is qjs at out/Default/qjs, and you run it as ./out/Default/qjs test.js.

### Can I debug PrimJS with Chrome DevTools?

Yes. The debugging support is a full implementation of the Chrome DevTools Protocol, which enables integration with Chrome Debugger for enhanced debugging of the running engine.

## Sources

- [License: Apache-2.0](https://github.com/lynx-family/primjs/blob/develop/LICENSE)
- [lynx-family/primjs on GitHub](https://github.com/lynx-family/primjs)
- [Project website](https://lynxjs.org)
- [README](https://github.com/lynx-family/primjs/blob/develop/README.md)
- [Releases](https://github.com/lynx-family/primjs/releases)

---

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