Skip to content

perf(android): call HybridView prop setters on the Java view directly - #1660

Open
ronickg wants to merge 1 commit into
margelo:mainfrom
ronickg:perf/android-view-direct-setters
Open

ronickg wants to merge 1 commit into
margelo:mainfrom
ronickg:perf/android-view-direct-setters

Conversation

@ronickg

@ronickg ronickg commented Sep 25, 2026

Copy link
Copy Markdown

On Android, every props update of every HybridView creates the view's C++ HybridObject and destroys it again. The generated updateViewProps resolves it with javaView->getJHybrid<T>Spec() only to call setters, and those forward to Kotlin through its _javaPart, which updateViewProps already has as javaView.

This used to be free: the updater called through javaView->cthis(). #1238 split out the CxxPart with a weak_ptr to fix the global-ref leak, and the updater switched to getJHybrid<T>Spec(). Since then nothing holds the object between updates, so each update:

  • takes the synchronized lookup and the getCxxPart() JNI call;
  • allocates the JHybrid<T>Spec and a JNI global ref;
  • destroys both afterwards.

The weak cache stays as it is. This PR only stops the updater from needing the object:

  • Direct setter calls: each changed prop calls its Kotlin setter directly on javaView, with the same method name, JNI signature and conversion as the generated C++ setter. The forward-call body is shared through getFbjniMethodCallBody, so both code paths generate from one place.
  • Includes: the updater .cpp includes the prop types' JNI conversion headers (enums, callbacks, optionals…).
  • hybridRef: the C++ HybridObject is resolved only when hybridRef changed and is handed to JS, as before fix: Fix Kotlin HybridObject jni::global_ref memory leak by separating CxxPart with weak_ptr #1238.

Regenerating react-native-nitro-test changes only the two *StateUpdater.cpp files; every JHybrid*Spec.cpp is byte-identical.

Measured

All numbers are from a Galaxy A22 (Android 13, arm64) running Release builds of apps/example, built from main with and without this change.

The workload is a local screen, not included in this PR: 1000 RecyclableTestViews without a hybridRef, with isBlue flipped on all of them 200 times. Main-thread CPU comes from /proc, three alternating runs per build:

main this PR
Main thread 10.34 / 10.42 / 10.52 s 8.25 / 8.34 / 8.60 s (−19%)
Whole process 29.4–29.8 s 27.7–28.5 s

simpleperf on the same run:

  • updateViewProps fell from 33.6% of main-thread samples to 14.6%.
  • On main, 61% of updateViewProps is resolving, creating and destroying the HybridObject; the setter JNI calls themselves are 5.8%.
  • With this change, most of what remains is getPropsFromStateWrapper.

The performance CI benchmarks JS↔native calls, not view prop updates, so it won't show this. I can add a view benchmark if that's wanted.

Validation

  • bun run build, bun specs, bun typecheck, bun lint, git diff --check: pass. I couldn't run lint-cpp/lint-swift/lint-kotlin locally, but no hand-written C++, Swift or Kotlin changes.
  • Harness on the A22 (nitro.views.harness.tsx + nitro.harness.ts):
    • 553/564 on both main and this change;
    • the same 11 fail on both: the native ArrayBuffer/HardwareBuffer tests, which need NDK API 26 on this build;
    • all TestView / RecyclableTestView tests pass, including "only calls native setters for changed Nitro props", "preserves an omitted native default…", recycling and remounting.
  • test: Cover View beforeUpdate/afterUpdate transaction semantics #1513's new transaction test fails identically with and without this change ("expected 3 to be 1"), so this change doesn't affect it.
  • Release (R8), 60 s of the example's View tab, three runs per build (views mounted and unmounted every 10 ms, a third with hybridRef, a callback and an enum):
    • no crashes;
    • refs still delivered and someMethod() works;
    • native heap and PSS after unmount are equal to or lower than main.
  • Lifetimes: a view without hybridRef no longer creates its CxxPart at all; dispose() already handles a missing one. The hybridRef path, and so what JS holds, is unchanged. iOS is not affected.

🤖 Generated with Claude Code

The generated `updateViewProps` resolved the view's C++ HybridObject
(`javaView->getJHybrid<T>Spec()`) on every props update, only to call
setters that forward to Kotlin through its `_javaPart`. Since the
`CxxPart` weak_ptr split (margelo#1238), nothing keeps that object alive between
updates, so each update created it (with a JNI global ref and the
`synchronized` lookup) and destroyed it again.

Generate each changed prop's call directly on `javaView`, with the same
method name, JNI signature and conversion as the C++ setter (the forward
body is now shared through `getFbjniMethodCallBody`), include the prop
types' JNI conversion headers in the updater, and resolve the C++
HybridObject only when `hybridRef` changed and is handed to JS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
nitro-docs Skipped Skipped Sep 25, 2026 10:14pm UTC

Request Review

@ronickg

ronickg commented Sep 26, 2026

Copy link
Copy Markdown
Author

Take with a grain of salt, just something claude found which I think might be interesting.

This branch was previously deployed

1 inactive deployment
Preview — d952333a Deployed Sep 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant