Repository navigation
RFC: ns metadata trim — ship only the native API metadata an app uses, on every NativeScript host
#62
Replies: 1 comment 1 reply
|
I believe this should be done after we settle #58 The bundler will then easily have enough information to get exactly the metadata you need. Then it should either generate the resulting metadata json before the native build or we can have something leveraging that that would generate the native-api-usage, allowing users to customize and add things as they need it. Having it all be a command with multiple arguments + keep + etc is shortsighted. A few use cases:
As is, I believe a command like |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC:
ns metadata trim— ship only the native API metadata an app uses, on every NativeScript hostandroid-react-nativens-metadata-symbols,objc-metadata-generatorfiltering andbuild-tools/metadata-filteron theandroid-react-nativebranch of NativeScript/runtimes@nativescript/react-nativeon macOS),nshintegration demo:nstudio/native-shell-with-ns(private for now)Summary
Every NativeScript app ships metadata for its platform's entire SDK, and most of it is never used. This RFC proposes one command that fixes that for any host running the NativeScript runtime:
evalmeans nothing is trimmed, rather than something being removed by guesswork.nshis the first host, and@nativescript/macos-node-api,@nativescript/react-nativeand standardnsprojects can follow without a new pipeline.native-api-usage.json, rather than replacing it. Declared rules from the app and its plugins are merged with what the analysis finds.Motivation
Metadata is the largest part of the runtime's weight. Measured on a showcase app embedding NativeScript in
nsh:Memory and startup fell along with the metadata, but how much each part of runtime startup contributes hasn't been profiled. On Android, filtering also enables R8. That took the APK from 23.8 to 21.4 MB and total memory from 50.0 to 40.3 MB, mostly from shrunk code, so the metadata's own effect on memory isn't isolated.
Embedders feel this most. When NativeScript is a capability added to another framework (
nsh, React Native), its footprint is judged on its own. "Adds 0.9 MB" and "adds 2.4 MB" are different conversations.The pieces exist, but aren't reachable as one feature. The
android-react-nativebranch has a Rust analyzer for Apple, a harvester and seed for Android, and generator closures.NS_METADATA_AUTO_FILTERworks inside an Xcode build phase and-PnsFilterMetadatainside Gradle. Hosts that don't use those build systems the same way, such as prebuilt frameworks, have no path at all, and the prebuilt framework still carries its full embedded metadata.Guide-level explanation
Apple
This writes
build/trim/metadata.ios.arm64.nsmd, the whitelist it used, and a copy of the framework with the trimmed metadata embedded and the binary shrunk to fit. Without--framework, you get the.nsmdto place yourself.Android
This writes
whitelist.mdg. Build with-PnsFilterMetadata, so the generator closes over dependent types and R8 keeps what JS reaches.Keeping or removing specific APIs
Existing
native-api-usage.jsonfiles keep working. In a NativeScript project, the app's file and its plugins' files are found automatically for iOS and Android. Other hosts pass them explicitly, and one-off additions can go on the command line:ns metadata trim macos dist/bundle.js --native-api-usage native-api-usage.json ... ns metadata trim ios dist/bundle.js --keep 'MapKit:*' ...When it refuses
A bundle that aliases
globalThis, reads a global through a computed name, or usesevalcan't be bounded:typeof NSStatusBar !== "undefined"and direct references analyze cleanly, so feature detection doesn't need to change.Relationship to
native-api-usage.jsonMetadata filtering and this command use the same mechanism: the same
Module:Symbolpatterns, and the samewhitelist.mdgandblacklist.mdgsemantics in the generators. They are two layers:native-api-usage.jsonns metadata trimnsproject's Xcode or Gradle buildblacklistblacklistundefinedat run timeThe merge follows the rules
ns preparealready applies:One difference is deliberate. An app with no file of its own still keeps what its plugins declare, because declarations here can only add to the analysis. Plugin blacklists apply only when an app opts in.
A plugin that reaches an API by a computed name is exactly the case analysis can't see, and its existing
usesentry covers it. So this layering makes declarations more useful rather than obsolete.A
blacklistentry can't remove a type a kept API's signature needs. On Apple platforms the generator keeps the type, and on Android the build fails and names it. Anyone blacklisting a name for confidentiality should check the generated metadata.Where this adds the most. Hosts with a prebuilt framework have no other path, since rules alone can't change metadata already embedded in the binary. In a standard
nsproject the overlap is larger:NS_METADATA_AUTO_FILTERand-PnsFilterMetadataautomate much of this inside Xcode and Gradle, and this command isn't wired intons buildyet. Its value there is one entry point and one set of safety rules.Reference-level details
Pipeline
ns-metadata-symbols(Oxc)seed.js/harvest.jsobjc-metadata-generatorresolveRefsenforceClosure__objc_metadatain a framework copy, or leave the.nsmdwhitelist.mdgThe extension point
Everything host- or platform-specific sits behind one interface:
How hosts adopt it
nsh.nsh/out/bundle.js__objc_metadata; Androidassets/metadata@nativescript/macos-node-apiNativeScript.framework--frameworkwas exercised with this package's framework, insidensh. Not run in a plain Node process@nativescript/react-native(runtimes#105)metadata.{ios,macos}.{arch}.nsmd, resolved bybundledMetadataPathnsprojectsplatforms/*/.../appafter webpack or Vite-sectcreate, Androidassets/metadataprepareorns buildyetEach new host needs to answer two questions: where is the emitted JS, and where does metadata go. The analysis, closure and safety rules don't change.
Rewriting the framework
__objc_metadatais immediately before__LINKEDIT, and__LINKEDITis the only segment that can safely move because it's addressed by file offsets. The rewrite replaces the segment contents, page-aligns the new size, moves__LINKEDITdown, rebases every load-command offset that points into it, and signs the binary ad hoc. Layouts it can't safely handle (fat binaries, a segment that isn't adjacent, metadata larger than the segment) are errors. Apps re-sign during their normal build.Format compatibility
The CLI reads the format version from the framework and from the generated file, and refuses metadata newer than the framework can read. This matters today: the published
macos-node-apiandios-node-apipackages read format 1, whileandroid-react-nativewrites format 2. A mismatch would otherwise exit at launch withInvalid metadata header version.Categories
Testing on iOS exposed a generator bug. Category cursors are spelled with the category's own name, so a whitelist naming
UIViewdropped everything declared inUIViewGeometry, andUIView.framebecame read-only. NativeScript/runtimes@cb26a2b exempts categories from the name filter. They only attach to classes that are kept, so this can't widen the class set.Drawbacks
Alternatives considered
NS_METADATA_AUTO_FILTER/-PnsFilterMetadatanative-api-usage.jsonaloneAdoption plan
nshintegration, as the first host and test bed (done).--analyzerand--generatorbecome optional.@nativescript/macos-node-api: verify in a plain Node process and document it.@nativescript/react-native: an install step for.nsmdresources, after runtimes#105.nsprojects: a config option wired intoprepare, off by default until it has run on real apps.Unresolved questions
--keepexists?ns metadata trim, or something underns build?All reactions