Skip to content

About

An experimental library enabling the use of the primjs engine from Lynx library

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

react-native-primjs-engine

An experimental library enabling the use of the PrimJS engine, which is used in Lynx—as a replacement for the default Hermes engine.

The engine is built from source as part of your app. No prebuilt binaries. Built and tested against React Native 0.87 with the New Architecture.

How it compares to Hermes

Measured with the example app in release mode on React Native 0.87.1, each engine running its own bytecode. The chart shows how many times faster one engine ran each test than the other.

PrimJS and Hermes on an iPhone 15 Pro: PrimJS is faster in 15 of 18 tests, Hermes is faster in RegExp, RayTrace and Box2D
PrimJS Hermes
Time to interactive 68.3 ms 63.3 ms
Memory after startup 45.4 MB 38.9 MB
PrimJS and Hermes on a nubia NX733J: PrimJS is faster in every Octane test, Hermes is slightly faster in most SunSpider tests
PrimJS Hermes
Time to interactive 60.8 ms 62.7 ms
Memory after startup 99.5 MB 91.0 MB

Installation

npm install react-native-primjs-engine
cd ios && pod install

The library is autolinked. Four things then have to change in the app.

1. Babel

PrimJS implements roughly ES2020. The React Native preset leaves newer syntax untouched because Hermes understands it, so add two transforms to babel.config.js:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    '@babel/plugin-transform-logical-assignment-operators',
    '@babel/plugin-transform-class-static-block',
  ],
};
npm install --save-dev @babel/plugin-transform-logical-assignment-operators @babel/plugin-transform-class-static-block

Newer built-in functions (Array.prototype.at, Object.hasOwn, String.prototype.replaceAll, findLast, toSorted and friends) are added by the runtime at startup; see cpp/Polyfills.h.

2. Android

android/gradle.properties:

hermesEnabled=false

MainApplication.kt:

import com.primjsengine.PrimJSRuntimeFactory

override val reactHost: ReactHost by lazy {
  getDefaultReactHost(
    context = applicationContext,
    packageList = PackageList(this).packages,
    jsRuntimeFactory = PrimJSRuntimeFactory(),
  )
}

android/app/build.gradle: remove the else { implementation jscFlavor } branch from dependencies. With Hermes disabled the template would otherwise pull in JavaScriptCore.

3. iOS

AppDelegate.swift:

import PrimJSEngine

class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
  // ...
  override func createJSRuntimeFactory() -> JSRuntimeFactoryRef {
    jsrt_create_primjs_factory()
  }
}

ios/.xcode.env, so that the bundle is not compiled to Hermes bytecode:

export USE_HERMES=false

The Hermes pod stays installed, because React Native 0.87 has no supported way to leave it out. It is not used.

4. Bytecode

Optional, and what makes startup competitive. See Bytecode.

Android, in android/app/build.gradle, after the React Native plugin:

apply from: "../../node_modules/react-native-primjs-engine/android/bytecode.gradle"

iOS, in the Podfile, followed by pod install:

require_relative '../node_modules/react-native-primjs-engine/scripts/primjs_pods'

target 'MyApp' do
  # ...
  primjs_compile_bundle!
end

Checking that it works

PrimJSInternal is the counterpart of HermesInternal:

console.log(globalThis.PrimJSInternal);
// { version: '2.11.1-rc.1', commit: 'f78a8bb…', gc: true }

It is undefined when the app is running on another engine.

Bytecode

tools/primjsc is to PrimJS what hermesc is to Hermes: a program for the build machine that compiles a JS bundle to the engine's bytecode. The build steps above run it on the bundle Metro produced, and the runtime recognises the result by its header and loads it instead of parsing source. Source bundles keep working, so debug builds and Metro are unaffected.

The effect on startup, measured with the example app (a 1.3 MB bundle): time from process start until the bundle has been evaluated and React starts the app, as the median of repeated launches.

Launches Median Quartiles
PrimJS, source bundle 20 328 ms 304 to 348 ms
PrimJS, bytecode 60 156 ms 142 to 175 ms
Hermes 30 155 ms 131 to 169 ms

Memory in use after startup went from about 110 MB to about 70 MB in the same app, because nothing has to be parsed.

To keep the bundle as source, pass -PprimjsBytecode=false on Android or put export PRIMJS_BYTECODE=false in ios/.xcode.env.

primjsc also works by hand:

scripts/compile-bundle.sh path/to/index.android.bundle   # builds primjsc if needed, compiles in place
build/primjsc/primjsc --source-url index.bundle -o out.bc in.js

Memory modes

PrimJS can be built two ways, and the choice is made at compile time.

Tracing GC Reference counting
Interpreter pre-generated arm64 assembly (the "template interpreter") portable C++
Architectures arm64 all
fib(30) on an M-series Mac about 23 ms about 110 ms

The GC build is the default wherever it is available: arm64 iOS devices and simulators, and arm64-v8a on Android. Other architectures fall back to reference counting automatically. To force reference counting everywhere:

  • Android: primjsEnableGC=false in gradle.properties.
  • iOS: PRIMJS_ENABLE_GC=0 pod install.

Bytecode is the same in both modes.

Limitations

  • No debugger. React Native DevTools cannot attach. console.log and LogBox work.
  • No Intl.
  • ES2020 baseline. Newer syntax needs Babel, newer built-ins need a polyfill. Error.cause and the RegExp d and v flags are not supported.

Example app and benchmark

example/ is a React Native 0.87.1 app that runs a fixed sequence of measurements on launch and shows the result as a table and as JSON. The tests come from react-native-engine-benchmark. The ones reported here are startup time, memory, and a set of Octane and SunSpider programs.

cd example && npm install
npx react-native run-android --mode release
cd ios && pod install && cd .. && npx react-native run-ios --mode Release

License

MIT, see LICENSE.

The vendored engine in cpp/primjs is PrimJS by the Lynx authors, licensed under Apache-2.0, and is in turn based on QuickJS by Fabrice Bellard and Charlie Gordon. See cpp/primjs/LICENSE and cpp/primjs/NOTICE.

About

An experimental library enabling the use of the primjs engine from Lynx library

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages