Skip to content

Avoid per-child dynamic_casts during Yoga layout - #59643

Open
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:yoga-layout-skip-dynamic-casts
Open

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:yoga-layout-skip-dynamic-casts

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

YogaLayoutableShadowNode runs three dynamic_casts for children on every layout pass:

  • resolveErrata checks dynamic_cast<const LayoutConformanceShadowNode *>(this). configureYogaTree calls it for every child to decide whether the child has to be configured again, so it runs on every layout, and it fails for almost every node, which is the slowest case of a dynamic_cast.
  • getContentBounds checks dynamic_cast<const ViewShadowNode *> for every child to read its hitSlop. It fails for every child that isn't a View (Text, ScrollView, Image, …).
  • shadowNodeFromContext gets the shadow node back from a Yoga node's context with a dynamic_cast. It runs for every child in layout() and getContentBounds(), and in Yoga's measure, baseline and clone callbacks.

Current behavior. In Time Profiler traces of JS-driven updates on an iPhone, __dynamic_cast took 11–18% of the JS thread, most of it inside layoutTree, and the type comparisons in those traces go through strcmp.

With this change. LayoutConformanceShadowNode and ViewShadowNode are both final, and their nodes are only created by descriptors that report the class's own Handle(), so comparing getComponentHandle() with that handle gives the same answer as the dynamic_cast; a static_cast follows. The Yoga node's context is only set by this class's two constructors, to this, so shadowNodeFromContext recovers it with a static_cast of the stored pointer. The behavior doesn't change.

Changelog:

[GENERAL] [CHANGED] - Yoga layout no longer uses dynamic_cast for every child to check its type

Test Plan:

  • The pinned clang-format passes on the changed file, and it compiles for iOS (arm64).
  • yarn fantom (full suite, tester rebuilt with cmake from this change, then from main): no test fails with this change that doesn't also fail on main in the same local environment. The local failures (LogBox and expect snapshot formatting, Text-itest, EventTimingAPI-itest) happen with and without this change.
  • Measurement. RNTester built with xcodebuild in Release and run on a physical iPhone 15 Pro (iOS 26.6.1). Both arms are the same binary, with this change switched by an environment variable read once. 8 launches in the order before, after, after, before, after, before, before, after; in each, 12 s of Time Profiler per scenario, counting JS-thread samples. Interactions were driven by Argent on the physical iPhone. The phone warmed up during the runs, so the totals of the same arm vary by up to 30%; the time in dynamic_cast is the most direct number, and the medians are given for context.

RNTester Playground, 30 Texts animated with Animated (JS driver), medians of 4 launches per arm:

Scenario before this PR change
Color animated dynamic_cast inside layout, per 12 s 317 ms 1.5 ms −99.5%
dynamic_cast, share of the JS thread 17.0% 4.9%
Yoga layout (layoutTree), per 12 s 1,144 ms 836 ms −26.9%
JS thread, per 12 s 2,454 ms 2,154 ms −12.2%
Color and font size animated dynamic_cast inside layout, per 12 s 338 ms 1.5 ms −99.6%
dynamic_cast, share of the JS thread 11.1% 3.0%
Yoga layout (layoutTree), per 12 s 2,618 ms 2,166 ms −17.3%
JS thread, per 12 s 4,098 ms 3,562 ms −13.1%

The dynamic_cast time left with this change (about 3–5% of the JS thread) comes from other places, such as adoptYogaChild and the shadow node constructors, which this PR doesn't change.

On Android these casts cost much less, since there the type comparison compares pointers: in the same color-animation scenario on an Android emulator, dynamic_cast inside layout went from 1.5% to 0.2% of the JS thread.

🤖 Generated with Claude Code

`resolveErrata` and `getContentBounds` check whether a child is a
`LayoutConformanceShadowNode` or a `ViewShadowNode` with a `dynamic_cast`,
which fails for almost every node. Both classes are final, so compare the
component handle instead. `shadowNodeFromContext` casts the Yoga node's
context, which only this class's constructors set (to `this`), so a
`static_cast` recovers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 29, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 29, 2026
@meta-codesync

meta-codesync Bot commented Oct 29, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D128421875.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants