VerifiedMeasured 2026-07-25 · task suite v1.0.0 · claude-sonnet-5

NativeScript vs. LynxJS

The same iOS app — same spec, same UI, two deep native features — built by isolated AI agents in both frameworks, with every token metered and the finished products performance-profiled.

5 trials per framework · 30/30 phases build-green · every number below is generated from the published raw data — nstudio/token-economics

Output tokens per app
1.9×

LynxJS 153.1K vs NativeScript 81.7K median — and the per-trial ranges never overlap.

Estimated cost per app
2.4×

LynxJS $18.29 vs NativeScript $7.52 median, list-price estimate.

Native-language code written
226 vs 0

Lines of Swift per app. Both agents wrote the same 16 lines of plist/entitlement config; only LynxJS also authored a native module. JS/Vue volume was equal.

Second native feature
4.0×

Phase 3 is the mechanism: bridge cost recurs per feature; direct-access cost amortizes after the first.

01Every trial, no overlap — total output tokens

Each dot is one complete app build (three phased agent sessions, summed). The most expensive NativeScript trial used fewer output tokens than the cheapest LynxJS trial — clean separation at n=5 per side.

NativeScriptLynxJS
each dot = one complete app build · output tokens
NativeScriptmedian 81,697
LynxJSmedian 153,110
no overlap
050K100K150K200K
02The mechanism — bridge cost recurs, direct access amortizes

After learning the direct-TypeScript platform pattern in phase 2, the NativeScript agent’s second native feature cost 71% less — Speech needed 2 native lines, both config, and zero Swift. The Lynx agent wrote the same 2 config lines plus a second complete Swift module (106 lines) with bridge registration and streaming-callback plumbing, so its phase 3 stayed as expensive as its phase 2 (−9%).

NativeScriptLynxJS
output tokens · median of 5 trials
UI shellLynxJS 1.7× NativeScript
NS Lynx
12.9K
22.5K
HealthKitLynxJS 1.3× NativeScript
NS Lynx
52.8K
67.5K
SpeechLynxJS 4.0× NativeScript
NS Lynx
15.4K
61.7K
017.5K35.0K52.5K70.0K
03Where the tokens go

The four usage buckets are always reported separately: cache reads dominate raw counts in multi-turn agentic sessions (the Lynx side re-read vendored Pods/Sparkling sources heavily in place of docs lookups), which is why a single collapsed “total tokens” would drown the signal. Output tokens — the agent’s actual work — are the headline metric.

NativeScript15,830,028 all buckets
Input · 0.00%Output · 0.52%Cache read · 98%Cache write · 2%
LynxJS42,163,122 all buckets
Input · 0.00%Output · 0.36%Cache read · 98%Cache write · 1%
04What you get — runtime performance of the finished apps
App size on disk
−49%

LynxJS 26.1 MB vs NativeScript 51.1 MB as an unsigned arm64 device archive — and still 5.3 MB of that is the DevTool stack Sparkling ships in Release.

Idle memory (RSS)
+34%

LynxJS 288 MB vs NativeScript 215 MB on the same screen; physical footprint agrees (+26%).

Cold launch
tie

316 vs 317 ms to foreground-active; both idle at 0.0% CPU.

API-path latency
wash

HealthKit query is a dead tie; Lynx wins the write→refresh round trip by ~30%.

Corrected 2026-08-02: The app-size finding was wrong, and reversed on re-measurement. What changed →

Performance metrics: View full performance data →

05The two interop models
Runtime reflection over native metadata

NativeScript

Direct reflection bridge over the native runtime

NativeScript runs JS/TS directly against the native platform runtimes — V8 on iOS and Android — and exposes native classes to JavaScript through metadata generated at build time from the platform SDKs. There is no fixed allow-list of wrapped components: in principle, any public Android or iOS API is reachable directly from JS by referencing its fully-qualified native name (for example, constructing an android.widget.TextView or calling into UIKit types directly).

Full TypeScript declarations for the native API surface are generated ahead of time (via @nativescript/types), so an agent working with those types in context has precise signatures available. The tradeoff is vocabulary size: native namespaces are long, platform-specific, and often diverge between iOS and Android, so a cross-platform call typically means writing and branching two native code paths rather than one shared call.

Strengths

  • +No bridge-authoring step for capabilities already in the native SDK — call it directly
  • +Complete, generated TypeScript typings for the native surface when included in context
  • +Single-file reach: a novel native call can usually be added without touching native-language source

Friction points

  • Verbose, fully-qualified native symbol names inflate output tokens per call site
  • iOS and Android APIs diverge, so cross-platform code often means two branches, not one
  • Correctness depends on knowing native marshalling/threading rules (e.g. main-thread dispatch) that aren’t enforced by the type system
NativeScript docs ↗
Authored Native Modules with a defined JS-facing API

LynxJS

Explicit native module bridge

LynxJS renders through its own engine and dual-thread architecture rather than a WebView, and extends into native platform code through Native Modules: a native class (Kotlin/Java, Swift/Obj-C) explicitly registers the methods it wants to expose, and JS calls into it through a defined module API. There is no arbitrary reflection over the native SDK — if a capability hasn’t been wrapped in a module, it isn’t callable from JS yet.

This narrows the vocabulary an agent needs on the JS side to whatever a module’s author decided to expose, which should make call sites for already-wrapped capabilities short and framework-idiomatic rather than platform-specific. The cost moves elsewhere: reaching a capability with no existing module means writing genuine native-language bridge code and registering it, spanning multiple files and at least two languages before any JS call can be made.

Strengths

  • +Terse, curated JS-facing API for already-wrapped capabilities — no native vocabulary required at the call site
  • +A narrower interop surface is easier to keep consistent across iOS and Android from the JS side
  • +Native module boundaries are explicit, which may reduce misuse of platform threading/marshalling rules

Friction points

  • No existing module means writing and registering native bridge code before any JS call is possible
  • Bridge authoring spans multiple files and languages, which likely costs more total tokens and more agentic tool-call round-trips for genuinely new capabilities
  • Younger ecosystem (open-sourced 2025) means fewer pre-built modules to fall back on today
LynxJS docs ↗
06Held constant across both frameworks
  • Model (claude-sonnet-5) and agent harness (headless Claude Code 2.1.220), pinned for every session
  • Byte-identical SPEC.md and audio fixture committed to both repos; every user-visible label pinned exactly
  • Structurally identical CLAUDE.md files — same sections, same level of help, no implementation hints
  • One official docs MCP server per framework, and nothing else, via strict MCP config
  • Fresh isolated session per phase (throwaway config dir: no memory, no personal settings)
  • Per-phase turn cap (160), build gates run by the harness — never self-reported by the agent
  • Interleaved trial order (NS, Lynx, NS, …), strictly sequential, same machine and Xcode 26.5
  • No hidden retries: every started trial is in the published record
07The task suite
01

Phase 1 — App shell (shared Vue 3 UI)

Home screen plus two placeholder feature screens with working navigation, exact labels pinned by spec. No native calls — both frameworks write Vue 3, isolating baseline framework fluency.

02

Phase 2 — HealthKit (first native feature)

Step-count read/write authorization, logging quantity samples, and a 7-day statistics query rendered as a list. Entitlements, permission flows, and async native data marshaling.

03

Phase 3 — Speech (second native feature)

Transcribe a bundled audio clip with streaming partial results into the UI. The second native feature reveals whether platform-access cost amortizes (direct access) or recurs (per-feature bridge authoring).

04

Acceptance & runtime performance

Operator checklist against the pinned spec, then Release rebuilds of every implementation profiled for size, memory, launch, and tap-to-result platform-API latency on one fixed simulator.

08Raw data — all 30 measured phases

Per trial, per phase — the rows behind every chart above. Full transcripts, diffs, and manifests are in the repository.

TrialPhaseOutputEst. costTurnsWall (s)DocsJS LOCNative LOC
main-ns-1111,466$1.17542187510
251,535$5.141241,007720015
315,403$1.214029351252
main-ns-218,979$0.95491693520
253,760$3.45807991518414
314,747$0.873317241032
main-ns-3112,932$1.29522244590
259,840$6.561529721016915
312,158$0.82331530992
main-ns-4116,571$1.92774217580
252,794$5.851478841320313
326,538$2.747148771353
main-ns-5116,198$1.62582568490
247,439$2.47605161224813
318,060$1.25392495932
main-lynx-1119,850$1.614824011370
267,499$6.631338633124127
365,761$9.611691,172784108
main-lynx-2122,525$2.567833422310
255,474$4.8788849385148
361,660$10.861841,16367794
main-lynx-3126,324$3.399045631550
2113,124$15.611614,1345101136
354,119$6.931541,029279100
main-lynx-4118,372$1.605125211820
2111,714$12.802091,6373184241
346,301$6.131127583137180
main-lynx-5124,710$2.597637711730
257,221$4.96956543123218
364,186$7.861491,049296165
09Go deeper
Replicate it

Every artifact behind these numbers is published.

Baselines, per-trial diffs and transcripts, build logs, perf JSON, and the harness that produced them. Run it against a different model, a new framework version, or your own framework pair — and submit the results.