Pull Request Overview
- Opened on October 21, 2026
- Status Closed
- Commit count 2 with first commit October 21, 2026
Total Delta
Open Days
Test Delta
How long has this pull request spent in each phase of its lifecycle?
| Fraction of total time | Business days | Phase |
|---|---|---|
|
|
1 day | Authoring 1 commit before pull request opened for review |
|
|
0 days | Awaiting first review |
|
|
0 days | Revising work with 1 commit in response to 1 review that left 5 comments |
Total time for pull request 1 business day from first commit to closed PR
Use a virtual accessor instead of a dynamic_cast when building ShadowView layout metrics
Summary:
ShadowView's constructor reads the node's layout metrics through layoutMetricsFromShadowNode, which uses dynamic_cast<const LayoutableShadowNode *> to find out whether the node has layout. The differ builds a ShadowView for every child it visits when it slices a node's children, so this runs for every visited node on every commit, including the per-frame commits that update a ScrollView's content offset.
Current behavior. Every call pays for a dynamic_cast, which walks the type's inheritance information at runtime; for a View, the node's type is ConcreteViewShadowNode<...>, several levels below LayoutableShadowNode. In Time Profiler traces of scrolling, __dynamic_cast took 19β21% of the differ's time.
With this change. ShadowNode gets a virtual asLayoutable() that returns nullptr, and LayoutableShadowNode overrides it to return this. layoutMetricsFromShadowNode calls it instead of the dynamic_cast, so every node, not only Views, gets its layout metrics without a runtime type check, and the compiler guarantees the result. LayoutableShadowNode::getDebugProps is marked override, since clang requires it once the class has another override.
Changelog:
[GENERAL] [CHANGED] - Building a ShadowView no longer uses a dynamic_cast to read a node's layout metrics
Test Plan:
- The pinned
clang-formatpasses on the changed files. The C++ API snapshots inscripts/cxx-api/api-snapshotslist the new method. -
yarn fantom(full suite, with the tester rebuilt from this change): 3,289 tests pass (110 skipped by their configuration). In that run the tester crashed (SIGSEGV) in two other files,AnimatedBackend-itestandStaleEventHandlersFromInterruptedRender-itest; neither crashed in 30 runs each on their own, and the same kind of crash happens locally without this change. -
Measurement. RNTester built withxcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1). The two arms are the same binary with the change switched by a launch argument, in the order before, after, after, before. Interactions were driven by Argent on the physical iPhone. Timed: the diff phase of each mount transaction fromTransactionTelemetry, on the main thread.
RNTester Playground
before
this PR
change
A FlatList of 500 multi-line messages, 20 flings down and 20 up per launch (~8,700 transactions): diff time per launch
1,140 ms
1,006 ms
β11.8%
Same, diff time per 1β2-mutation transaction (the per-frame scroll updates)
111.0 Β΅s
98.3 Β΅s
β11.4%
1000 labels mounted and unmounted, 10 times per arm: diff time per mount or unmount (median)
2.61 ms
2.47 ms
β5.4%
π€ Generated with Claude Code
Comments Threads Pending Resolution
Resolved Comment Threads
No resolved comments have been left on this PR.