Greetings, hackers tall and hackers small!
We're only a few articles in to this series on mobile application development frameworks, but I feel like we are already well into our journey. We started our trip through the design space with a look at Ionic /Capacitor,which defines its user interface in terms of the web platform, and onlycalls out to iOS or Android native features as needed. We proceededon to ReactNative,which moves closer to native by rendering to platform-provided UIwidgets, layering a cross-platform development interface on top.
Today's article takes an in-depth look at NativeScript, whose point in the design space is further on the road towardsthe platform, unabashedly embracing the specificities of the APIavailable on iOS and Android, exposing these interfaces directly to theapplication programmer.
In practice what this looks like is that a NativeScript app is a nativeapp which simply happens to call JavaScript on the main UI thread. ThatJavaScript has access to all native APIs, directly, without themediation of serialization or message-passing over a bridge or messagequeue.
The first time I heard this I thought that it couldn't actually be allnative APIs. After all, new versions of iOS and Android come out quitefrequently, and surely it would take some effort on the part ofNativeScript developers to expose the new APIs to JavaScript. But no,it really includes all of the various native APIs: the NativeScriptdevelopers wrote a build-time inspector that uses the platform's nativereflection capabilities to grovel through all available APIs and toautomatically generate JavaScript bindings, with associated TypeScripttype definitions so that the developer knows what is available.
Some of these generated files are checked into source, so you can get anidea of the range of interfaces that are accessible to programmers; forexample, see the iOS type definitions forx86-64.There are bindings for, like, everything.
Given access to all the native APIs, how do you go about making an app?You could write the same kind of programs that you would in Swift orKotlin, but in JavaScript. But this would require more than just theability to access native capabilities when needed: it needs a thoroughknowledge of the platform interfaces, plus NativeScript itself on top.Most people don't have this knowledge, and those that do are probablyprogramming directly in Swift or Kotlin already.
On one level, NativeScript's approach is to take refuge in that mostecumenical of adjectives, "unopinionated". Whereas Ionic / Capacitorencourages use of web platform interfaces, and React Native only reallysupports React as a programming paradigm, NativeScript provides alow-level platform onto which you can layer a number of differenthigh-level frameworks.
Now, most high-level JavaScript application development frameworks areoriented to targetting the web: they take descriptions of userinterfaces and translate them to the DOM. When targetting NativeScript,you could make it so that they target native UI widgets instead.However given the baked-in assumptions of how widgets should be laid out(notably via CSS), there is some impedance-matching to do betweenDOM-like APIs and native toolkits.
NativeScript's answer to this problem is a middle layer: across-platform UIlibrarythat provides DOM-like abstractions and CSS layout in a way that bridgesthe gap between web-like and native. You can even define parts of theUI using a NativeScript-specific XMLvocabulary,which NativeScript compiles to native UI widget calls atrun-time.Of course, there is no CSS engine in UIKit or Android's UI toolkit, soNativeScript includes its own, implemented in JavaScript ofcourse.
You could program directly to this middle layer, but I suspect that itsreal purpose is in enabling Angular, Vue, Svelte, or the like. Thepitch would be that NativeScript lets app developers use pleasanthigh-level abstractions, but while remaining close to the native APIs;you can always drop down for more power and expressiveness if needed.
Diving back down to the low level, as we mentioned all of theinteractions between JavaScript and the native platform APIs happen onthe main application UI thread. NativeScript does also allowprogrammers to create background threads, using an implementation ofthe Web WorkerAPI.One could even in theory run a React-based UI in a worker thread andproxy native UI updates to the main thread; as an unopinionatedplatform, NativeScript can support many different frameworks andparadigms.
Finally, there is the question of how NativeScript runs the JavaScriptin an application. Recall that Ionic / Capacitor uses the native JSengine, by virtue of using the native WebView, and that React Nativeused to use JavaScriptCore on both platforms but now uses its own Hermesimplementation. NativeScript is another point in the design space,using V8 on both platforms. (They used to use JavaScriptCore on iOS butswitched toV8once V8 was able to run on iOS in "jitless" mode.) Besides the reducedmaintenance burden of using a single implementation on all platforms,this also has the advantage of being able to use V8snapshots to moveJavaScript parse-and-compile work to build-time, even on iOS.
NativeScript is fundamentally simple: it's V8 running in anapplication's main UI thread, with access to all platform native APIs.So how do we expect it to perform?
In theory, applications with a NativeScript-like architecture shouldhave no problem with startup time, because they can pre-compile all oftheir JavaScript into V8 snapshots. Snapshots are cheap to load upbecause they are already in a format that V8 is ready to consume.
In practice, it would seem that V8 snapshots do not perform as expectedforNativeScript.There are a number of aspects about this situation that I don'tunderstand, which I suspect relate to the state of the tooling around V8rather than to the fundamental approach of ahead-of-time compilation.V8 is really made for Chrome, and it could be that not enoughmaintenance resources have been devoted to this snapshot facility.
In the meantime, NativeScript instead uses V8's code cachefeature, which caches the result ofparsing and compiling JavaScript files on the device. In this way thefirst time an app is installed or updated, it might start up slowly, butsubsequent runs are faster. If you were designing a new operatingsystem, you'd probably want to move this work to app install-time.
As we mentioned above, NativeScript apps have access to all native APIs.That is a lot of APIs, and only some of those interfaces will actuallybe used by any given app. In an ideal world, we would expect the buildprocess to only include JavaScript code for those APIs that are neededby the application. However in the presence of eval and dynamicproperty lookup, pruning the native API surface to the precise minimumis a hard problem for a bundler to perform on its own. The solution forthe time being is to manually allow and deny subsets of the platformnativeAPI.It's not an automatic process though, so it can be error-prone.
Besides the work that the JavaScript engine has to do to load anapplication's code, the other startup overhead involves whatever workthat JavaScript might need to perform before the first frame is shown.In the case of NativeScript, more work is done before the initial layoutthan one would think: the main UI XML file is parsed by an XML parserwritten in JavaScript, any needed CSS files are parsed and loaded (againby JavaScript), and the tree of XML elements is translated to a tree ofUIelements.The layout of the items in the view tree is then computed (inJavaScript, but calling into native code to measure text and so on), andthen the app is ready.
At this point, I am again going to wave my "I am just a compilerengineer" flag: I am not a UI specialist, much less a NativeScriptspecialist. As in compilers, performance measurement and monitoring arekey to UI development, but I suspect that also as in compilers there isa role for gut instinct. Incremental improvements are best driven bymetrics, but qualitative leaps are often the result of somewhatineffable hunches or even guesswork. In that spirit I can only surmisethat React Native has an advantage over NativeScript intime-to-first-frame, because its layout is performed in C++ and becauseits element tree is computed directly from JavaScript instead of havingJavaScript interpret XML and CSS files. In any case, I look forward tothe forthcoming part 2 of the NativeScript and React Native performanceinvestigationsthat were started in November 2022.
If I were NativeScript and using NativeScript's UI framework, and ifstartup latency proves to actually be a problem, I would lean intosomething in the shape of Angular's ahead-of-time compilationmode, but for the middleNativeScript UI layer.
On the face of it, NativeScript is the most jank-prone of the threeframeworks we have examined, because it runs JavaScript on the mainapplication UI thread, interleaved with UI event handling and paintingand all of that. If an app's JavaScript takes too long to run, the appmight miss frames or fail to promptly handle an event.
On the other hand, relative to React Native, the user's code is muchcloser to the application's behavior. There's no asynchrony between theapplication's logic and its main loop: in NativeScript it is easy toidentify the code causing jank and eventually fix it.
The other classic JavaScript-on-the-main-thread worry relates to garbagecollection pauses. V8's garbage collector does try to minimize thestop-the-world phase by tracing the heap concurrently and leveragingparallelism during pauses. Also, theuser interface of a mobile app runs in an event loop, and typicallyspends most of its time idle; V8 exposes some API that can takeadvantage of this idle time to perform housekeeping tasks instead ofneeding to do them when handling high-priority events.
That said, having looked into the code of both the iOS and Androidrun-times, NativeScript does not currently take advantage of thisfacility. I dug deeper and it would seem that V8 itself is in flux, asthe IdleNotificationDeadlineAPIis on its way out; is the thought that concurrent tracing is largelysufficient? I would expect that if conservative stackscanninglands, we will see a re-introduction of this kind of API, as it doesmake sense to synchronize with the event loop when scanning the mainthread stack.
As we have seen in our previous evaluations, this question boils down to"is the JavaScript engine state-of-the-art, and can it performjust-in-time compilation". In the case of NativeScript, the answers areyes and maybe, respectively: V8 is state-of-the-art, and it can JIT onAndroid, but not on iOS.
Perhaps the mitigation here is that the hardware that iOS runs on tendsto be significantly more powerful than median Android devices; if youhad to pick a subset of users to penalize with an interpreter-onlyrun-time, people with iPhones are the obvious choice, because they canafford it.
Recall that our perspective in this series is that of the designer of anew JavaScript-based mobile development platform. We are trying toanswer the question of what would it look like if a new platform offereda NativeScript-like experience. In this regard, only the structure ofNativeScript is of interest, and notably its "market success" is notrelevant, except perhaps in some Hayekian conception of the world in whichmarkets are necessarily smarter than, well, me, or you, or any one ofus.
It must be said, though, that React Native is the 800-pound gorilla ofJavaScript mobile application development. The 2022 State of JSsurveyshows that among survey respondents, more people are aware of ReactNative than any other mobile framework, and people are generally morepositive about React Native than other frameworks. Does NativeScript'smitigated market share indicate something about its architecture, ordoes it speak speak more to the size of Facebook's budget, both on thedeveloper experience side and on marketing?
Oddly, I think the answer to the market wisdom question might be foundin a 35-year-old computer science paper, "On the expressive power ofprogramminglanguages"(PDF).
In this paper, Matthias Felleisen considers the notion of what it meansfor one programming language to be more expressive than another. Forexample, is a language with just for less expressive than a languagewith both for and while? Intuitively we would say no, these aresimilar things; you can make a simple local transformation of while (x) {...} to for (;x;) {...} and you have exactly the same programsemantics. On the other hand a language with just for is lessexpressive than one which also has goto; there is no simple localrewrite that can turn goto into for.
In the same way, we can consider the question of what it would mean forone library to be more expressive than another. After all, the API of alibrary exposes a language in which its user can write programs; weshould be able to reason about these languages. So between React Nativeand NativeScript, which one is more expressive?
By Felleisen's definitions, NativeScript is clearly the more expressivelanguage: there is no simple local transformation that can turnimperative operations on native UI widgets into equivalentfunctional-reactive programs. Yes, with enough glue code React Nativecan reach directly to native APIs in a similar way as NativeScript, buteverything that touches the native UI tree is expressly under ReactNative's control: there is no sanctioned escape hatch.
You might think that "more expressive" is always better, but Felleisen'stake is more nuanced than that. Yes, he says, more expressive languagesdo allow programmers to make more concise programs, because they allowprogrammers to define abstractions that encapsulate patterns, and thisis a good thing. However he also concludes that "an increase inexpressive power is related to a decrease of the set of 'natural'(mathematically appealing) operational equivalences." Less expressiveprogramming languages are easier to reason about, in general, and indeedthat is one of the recognized strengths of React's programming model: itis easy to compose components and have confidence that the result willwork.
A NativeScript-like architecture offers the possibility of performance:the developer has all the capabilities needed for writingpleasant-to-use applications that blend in with the platform-nativeexperience. It is up to the developers to choose how to use the powerat their disposal. In the wild, I expect that the low-level layer ofNativeScript's API is used mainly by expert developers, who know how toassemble well-functioning machines from the parts on offer.
As a primary programming interface for a new JavaScript-based mobileplatform, though, just providing a low-level API would seem to be notenough. NativeScript rightly promotes the use of more well-knownhigh-level frameworks on top: Angular, Vue, Svelte, and such. Lessexperienced developers should use an opinionated high-level UIframework; these developers don't have good opinions yet and the APIshould lead them in the right direction.
That's it for today. Thanks for reading these articles, by the way; Ihave enjoyed diving into this space. Next up, we'll take a look beyondJavaScript, to Flutter and Dart. Until then, happy hacking!