Since its inception in 2015, NativeScript has leveraged V8 on Android - arguably the world's most powerful JavaScript engine to power its runtime. iOS became V8 focused in 2020 while previously had used JavaScriptCore. While V8 has served us exceptionally well, staying current with its rapid evolution has always had its challenges. To put this in perspective: our Android runtime currently uses V8 10.3, while V8 13.5 is already available in the wild.
Upgrading V8 to newer versions has always been an undertaking. V8 changed some critical Garbage Collection Callback APIs on which NativeScript relies for memory management. This requires further adjustments to aspects of the current runtimes, which is a substantial undertaking.
While we could tackle this challenge head-on, it raises broader strategic questions: What if V8 deprecates more crucial APIs in the future? What if they remove the ability to opt out of Intl APIs, forcing us to ship larger apps?
There have also been new JavaScript engines introduced over the years that are more optimized for mobile devices, with improved Time To Interactive (TTI) and smaller app sizes, which we'd like to extend to users.
These concerns have confirmed it just makes sense to evolve beyond a single-engine architecture. But the question has always been how?
This is where Node-API comes in. Node-API is a stable and ABI safe API for building native add-ons for Node.js. But more importantly, it operates completely free from any specific JavaScript runtime implementation, acting as a universal bridge between native code and JavaScript engines.
If the NativeScript Android runtime were based on Node-API, we could run NativeScript apps on any JavaScript engine that supported Node-API. We could update to the latest and greatest V8, switch to a mobile-optimized engine like Hermes, or even try out the up-and-coming QuickJS without having to rewrite the entire NativeScript Android runtime once for each engine.
Today, we're thrilled to announce that we have done exactly that – we've successfully rewritten the NativeScript Android runtime using Node-API and we are excited to share the new possibilities this opens up for us.
NativeScript is finally independent of the underlying JavaScript engine. This means that we can run NativeScript apps on any JavaScript engine that supports Node-API. We already have support for V8, Hermes, QuickJS, and JavaScriptCore (JSC) in the new Android runtime.
NativeScript can now be integrated with other cross-platform frameworks that use Node-API. We have been working on integrating NativeScript with React Native and Expo and we will be sharing more on this in the future.
Rewriting the NativeScript's android runtime has allowed us to deeply understand how the runtime works and we have been able to fix many long-standing memory leaks and issues. This has also equipped us to quickly update to the latest versions of the JavaScript engines as they are released.
The NativeScript's new android runtime will get continuous updates and improvements as we move forward.
One thing to clear up before we go any further is that V8 is not going anywhere (we love it ❤️). In fact we are planning to upgrade V8 to the latest version in the coming weeks. Keep using V8 if you prefer it, or switch to a different JS engine if you want to experiment with something new. It's totally up to you and your app requirements.
I know you are thinking, this is a big change and how am I going to upgrade my NativeScript apps to this new runtime? Who has the time to fix 100 breaking changings and build steps? Well you don't have to worry about that because there's zero breaking changes! Yes zero!
The new Android runtime is 100% backwards compatible with the old one. This means that you can update to the new runtime without having to change any of your code. It just works as a drop-in replacement.
We don't recommened shipping apps to production with the new runtimes just yet. This is an early preview release however we welcome you to try running your apps with any of the engines. Let us know if you run into anything via Discord
Since we are introducing Node-API between the JavaScript engine and the NativeScript runtime, the initial assumption might be that this would introduce some performance overhead. And it did at first, but we have been working hard over the past few weeks to optimize it full-tilt.
Here are some initial benchmarks we have been running and thrilled to share the results with you:
Benchmark | No. of Calls | Old Runtime | New Runtime (V8) | Change |
---|---|---|---|---|
Void Method on java instance | 1M | 322.4 ms | 254.0 ms | 21.2% faster |
Field on java instance | 1M | 569.6 ms | 940.0 ms | 65.0% slower |
Int Field on java instance | 1M | 281.6 ms | 656.0 ms | 133.0% slower |
Get Field | 1M | 1599.1 ms | 1472.0 ms | 8.0% faster |
Void Method | 1M | 1122.2 ms | 998.0 ms | 11.1% faster |
multiply(a,b) | 1M | 1330.0 ms | 1273.0 ms | 4.3% faster |
Pass and return string | 1M | 1099.6 ms | 1095.0 ms | 0.4% faster |
Return a String | 1M | 1446.3 ms | 1328.0 ms | 8.2% faster |
Return an Int | 1M | 1121.3 ms | 1025.0 ms | 8.6% faster |
Pass a String | 1M | 1667.4 ms | 1531.0 ms | 8.2% faster |
Return a Boolean | 1M | 1108.4 ms | 1018.0 ms | 8.2% faster |
Return a Double | 1M | 1151.4 ms | 1049.0 ms | 8.9% faster |
Pass an Int | 1M | 1281.7 ms | 1215.0 ms | 5.2% faster |
Pass a Double | 1M | 1281.5 ms | 1198.0 ms | 6.5% faster |
Pass a Boolean | 1M | 1314.3 ms | 1237.0 ms | 5.9% faster |
Invoke callback from java | 1M | 2046.2 ms | 1155.0 ms | 43.6% faster |
Return an Int Array | 10K | 109.2 ms | 150.0 ms | 37.4% slower |
Pass an Int Array | 10K | 23.9 ms | 30.0 ms | 25.5% slower |
Return a String Array | 10K | 106.1 ms | 151.0 ms | 42.3% slower |
Pass a String Array | 10K | 60.3 ms | 54.0 ms | 10.4% faster |
Return a Double Array | 10K | 89.0 ms | 148.0 ms | 66.3% slower |
Pass a Double Array | 10K | 40.6 ms | 24.0 ms | 40.9% faster |
Return a Boolean Array | 10K | 143.1 ms | 152.0 ms | 6.2% slower |
Pass a Boolean Array | 10K | 22.5 ms | 25.0 ms | 11.1% slower |
Return a Date Object | 10K | 71.7 ms | 171.0 ms | 138.5% slower |
Pass a Date Object | 10K | 96.1 ms | 176.0 ms | 83.1% slower |
The benchmarks were run on a OnePlus 9 device running Android 14. Overall, the new runtime shows remarkable performance in most common operations, with 11 out of 16 million-call benchmarks showing positive improvements. The core operations that most NativeScript apps rely on are notably faster, which should translate to better real-world performance for most applications.
While there are some areas that show performance regressions, particularly in array operations and complex object handling, we're optimizing these as well. Given that this is a preview release, we're confident these numbers will continue to improve as we fine-tune the implementation.
Most importantly, these benchmarks demonstrate that our Node-API based approach is not just viable - it's actually superior in many key scenarios, while providing the strategic benefits of engine independence and future-proofing 💪.
As mentioned earlier, the new Android runtime also supports Hermes, QuickJS and JSC. We know you are excited to see how these engines perform so here are some initial benchmarks:
Benchmark | No. of Calls | Old Runtime | QuickJS | Change |
---|---|---|---|---|
Void Method on java instance | 1M | 322.4 ms | 391.0 ms | 21.3% slower |
Field on java instance | 1M | 569.6 ms | 659.1 ms | 15.7% slower |
Int Field on java instance | 1M | 281.6 ms | 381.7 ms | 35.5% slower |
Get Field | 1M | 1599.1 ms | 877.8 ms | 45.1% faster |
Void Method | 1M | 1122.2 ms | 653.5 ms | 41.8% faster |
multiply(a,b) | 1M | 1330.0 ms | 935.9 ms | 29.6% faster |
Pass and return string | 1M | 1099.6 ms | 1221.3 ms | 11.1% slower |
Return a String | 1M | 1446.3 ms | 1059.4 ms | 26.8% faster |
Return an Int | 1M | 1121.3 ms | 672.2 ms | 40.1% faster |
Pass a String | 1M | 1667.4 ms | 1177.7 ms | 29.4% faster |
Return a Boolean | 1M | 1108.4 ms | 667.4 ms | 39.8% faster |
Return a Double | 1M | 1151.4 ms | 664.5 ms | 42.3% faster |
Pass an Int | 1M | 1281.7 ms | 878.8 ms | 31.4% faster |
Pass a Double | 1M | 1281.5 ms | 872.8 ms | 31.9% faster |
Pass a Boolean | 1M | 1314.3 ms | 859.1 ms | 34.6% faster |
Invoke callback from java | 1M | 2046.2 ms | 935.9 ms | 54.3% faster |
Return an Int Array | 10K | 109.2 ms | 78.6 ms | 28.0% faster |
Pass an Int Array | 10K | 23.9 ms | 17.3 ms | 27.6% faster |
Return a String Array | 10K | 106.1 ms | 87.9 ms | 17.2% faster |
Pass a String Array | 10K | 60.3 ms | 35.8 ms | 40.6% faster |
Return a Double Array | 10K | 89.0 ms | 83.4 ms | 6.3% faster |
Pass a Double Array | 10K | 40.6 ms | 14.7 ms | 63.8% faster |
Return a Boolean Array | 10K | 143.1 ms | 74.1 ms | 48.2% faster |
Pass a Boolean Array | 10K | 22.5 ms | 16.1 ms | 28.4% faster |
Return a Date Object | 10K | 71.7 ms | 108.0 ms | 50.6% slower |
Pass a Date Object | 10K | 96.1 ms | 108.2 ms | 12.6% slower |
Benchmark | No. of Calls | Old Runtime | Hermes | Performance Change |
---|---|---|---|---|
Void Method on java instance | 1M | 322.4 ms | 2029.0 ms | 529.3% slower |
Field on java instance | 1M | 569.6 ms | 1417.0 ms | 148.8% slower |
Int Field on java instance | 1M | 281.6 ms | 1119.0 ms | 297.4% slower |
Get Field | 1M | 1599.1 ms | 1592.0 ms | 0.4% faster |
Void Method | 1M | 1122.2 ms | 1328.0 ms | 18.3% slower |
multiply(a,b) | 1M | 1330.0 ms | 1600.0 ms | 20.3% slower |
Pass and return string | 1M | 1099.6 ms | 1209.0 ms | 10.0% slower |
Return a String | 1M | 1446.3 ms | 1652.0 ms | 14.2% slower |
Return an Int | 1M | 1121.3 ms | 1383.0 ms | 23.3% slower |
Pass a String | 1M | 1667.4 ms | 1902.0 ms | 14.1% slower |
Return a Boolean | 1M | 1108.4 ms | 1344.0 ms | 21.3% slower |
Return a Double | 1M | 1151.4 ms | 1335.0 ms | 16.0% slower |
Pass an Int | 1M | 1281.7 ms | 1524.0 ms | 18.9% slower |
Pass a Double | 1M | 1281.5 ms | 1527.0 ms | 19.2% slower |
Pass a Boolean | 1M | 1314.3 ms | 1498.0 ms | 14.0% slower |
Invoke void callback from java | 1M | 2046.2 ms | 1819.0 ms | 11.1% faster |
Return an Int Array | 10K | 109.2 ms | 118.0 ms | 8.1% slower |
Pass an Int Array | 10K | 23.9 ms | 28.0 ms | 17.2% slower |
Return a String Array | 10K | 106.1 ms | 135.0 ms | 27.2% slower |
Pass a String Array | 10K | 60.3 ms | 43.0 ms | 28.7% faster |
Return a Double Array | 10K | 89.0 ms | 121.0 ms | 36.0% slower |
Pass a Double Array | 10K | 40.6 ms | 37.0 ms | 8.9% faster |
Return a Boolean Array | 10K | 143.1 ms | 138.0 ms | 3.6% faster |
Pass a Boolean Array | 10K | 22.5 ms | 25.0 ms | 11.1% slower |
Return a Date Object | 10K | 71.7 ms | 175.0 ms | 144.1% slower |
Pass a Date Object | 10K | 96.1 ms | 228.0 ms | 137.3% slower |
NOTE: *
Calls on java instance are slower because the Hermes does not use our custom HostObject extension to Node-API yet and fallbacks to using JavaScript Proxy. We'll be addressing that soon.
Benchmark | No. of Calls | Old Runtime | JSC | Performance Change |
---|---|---|---|---|
Void Method on java instance | 1M | 322.4 ms | 6885.0 ms | 2035.5% slower |
Field on java instance | 1M | 569.6 ms | 4642.0 ms | 714.9% slower |
Int Field on instance | 1M | 281.6 ms | 3836.0 ms | 1262.2% slower |
Get Field | 1M | 1599.1 ms | 5747.0 ms | 259.4% slower |
Void Method | 1M | 1122.2 ms | 4493.0 ms | 300.4% slower |
multiply(a,b) | 1M | 1330.0 ms | 5849.0 ms | 339.8% slower |
Pass and return string | 1M | 1099.6 ms | 3264.0 ms | 196.8% slower |
Return a String | 1M | 1446.3 ms | 5544.0 ms | 283.3% slower |
Return an Int | 1M | 1121.3 ms | 4707.0 ms | 319.8% slower |
Pass a String | 1M | 1667.4 ms | 6018.0 ms | 260.9% slower |
Return a Boolean | 1M | 1108.4 ms | 4721.0 ms | 326.0% slower |
Return a Double | 1M | 1151.4 ms | 4687.0 ms | 307.1% slower |
Pass an Int | 1M | 1281.7 ms | 5132.0 ms | 300.4% slower |
Pass a Double | 1M | 1281.5 ms | 5114.0 ms | 299.1% slower |
Pass a Boolean | 1M | 1314.3 ms | 5154.0 ms | 292.2% slower |
Invoke callback from java | 1M | 2046.2 ms | 5384.0 ms | 163.1% slower |
Return an Int Array | 10K | 109.2 ms | 1210.0 ms | 1008.1% slower |
Pass an Int Array | 10K | 23.9 ms | 88.0 ms | 268.2% slower |
Return a String Array | 10K | 106.1 ms | 1235.0 ms | 1064.0% slower |
Pass a String Array | 10K | 60.3 ms | 171.0 ms | 183.6% slower |
Return a Double Array | 10K | 89.0 ms | 1682.0 ms | 1789.9% slower |
Pass a Double Array | 10K | 40.6 ms | 113.0 ms | 178.3% slower |
Return a Boolean Array | 10K | 143.1 ms | 1339.0 ms | 836.4% slower |
Pass a Boolean Array | 10K | 22.5 ms | 81.0 ms | 260.0% slower |
Return a Date Object | 10K | 71.7 ms | 798.0 ms | 1013.0% slower |
Pass a Date Object | 10K | 96.1 ms | 881.0 ms | 816.8% slower |
I was surprised to see JSC being the slowest of all in our benchmarks. I even went ahead and tried to make some perf improvements in Node-API implementation for JSC and got around 30-40% boost. However even then, JSC is still not performing well. Most of this overhead comes from doing Property access. We will investigate this further in the coming weeks as well.
Choosing a JS engine other than V8 can have a significant impact on your app size. Here are the app sizes for a simple "Hello World" app built with each of the supported engines:
Engine | App Size (MB) | % Difference |
---|---|---|
V8 | 11.3 | 0% |
V8 N-API | 11.7 | 3.5% bigger |
Hermes | 5.6 | 50.4% smaller |
QuickJS | 5.3 | 53.1% smaller |
JSC | 7.2 | 36.3% smaller |
Note that this is the size of the app with one architecture (arm64-v8a).
With metadata filtering and proguard enabled, I was able to get the app size down to just 3.7MB with QuickJS!
Memory usage is another important factor to consider when choosing a JS engine. Here are the memory usage benchmarks for each of the supported engines:
Engine | Memory Usage (MB) | % Difference |
---|---|---|
V8 N-API | 139 | 0% |
Hermes | 144 | 3.6% more |
QuickJS | 115 | 17.3% less |
JSC | 153 | 10.1% more |
The memory benchmarks were taken after running the app for 5 minutes on a OnePlus 9 device. The memory usage was measured using Android Studio's built-in memory profiler.
You are welcome to explore these benchmarks yourself on this ns-napi-benchmarks repo here.
The new Android runtime supports V8, Hermes, QuickJS and JSC. So which one should you choose? The answer is: it depends on your needs.
Every app is built differently and has different requirements. Hermes and QuickJS at first glance seem to be the best options due to their smaller app sizes and faster startup times. However, they may not be the best choice for apps that do heavy work on the main thread. V8 supports JIT compilation which can be beneficial for performance intensive taks, but it comes at the cost of a larger app size. However there are many apps that don't need V8 engine and can benefit from the smaller app size and faster startup times of Hermes or QuickJS.
Currently we recommend using V8 for most apps since it's well tested and stable, but we encourage you to experiment with the other engines and share your experience with us. Please report any issues you find with the new runtimes so we can fix them as soon as possible.
We don't recommened shipping apps to production with the new runtimes just yet. This is an early preview however we welcome you to try running your apps with any of the engines. Let us know if you run into anything via Discord
To update to the new Android runtime, simply run one of the following commands depending on the engine you want to try:
First make sure to run ns clean
after updating to clear the old runtime files from your project.
npm install -D @nativescript/android@napi-v8
npm install -D @nativescript/android@napi-quickjs
npm install -D @nativescript/android@napi-hermes
Hermes engine does not support many ES6+ features yet, so you will need to transpile your code to ES5 before running it on Hermes. Here's a how you can update your webpack.config.js
to do that:
webpack.chainWebpack((config) => {
config.module
.rule('load_files')
.test(/\.(js|ts|jsx|tsx)$/)
.use('babel-loader')
.before('ts')
.loader('babel-loader')
.options({
presets: ['module:metro-react-native-babel-preset'],
plugins: ['@babel/plugin-proposal-export-namespace-from'],
});
config.optimization.minimize(false);
});
And add the following deps to your package.json:
devDependencies: {
"babel-loader": "^9.2.1",
"metro-react-native-babel-preset": "^0.77.0",
"@babel/core": "^7.26.0",
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
}
Your app might need some additional babel plugins or presets depending on your codebase.
npm install -D @nativescript/android@napi-jsc
And finally run the app:
ns run android
That's it! Your app should now be running on the new Android runtime.
If you run into any issues, please report them on the new Android runtime's GitHub repo.
Please provide as much feedback or reports on issues as you'd like here.
The new Android runtime is currently in early preview and it already works with V8, Hermes, QuickJS and JavaScriptCore (JSC). Yes, even JSC works! Perhaps most significantly, this new architecture ensures immediate compatibility with any future JavaScript engine that implements the Node-API standard - truly future-proofing your NativeScript applications.
We are beginning the V8 update now with these new Node-API enabled engines which we'll announce soon once it's available. Additionally we are quite close on iOS as well!
Production, production, production! We are also doing a lot of production tests with all the engines and your help is always appreciated. Let us know how it goes for you!
Make sure to checkout the new android runtime's GitHub repo and star it to stay updated with the latest changes.