Skip to content

Reuse unconstrained FitContent height measurements - #2043

Open
tomekzaw wants to merge 5 commits into
react:mainfrom
tomekzaw:measure-cache-unconstrained-fit-content
Open

tomekzaw wants to merge 5 commits into
react:mainfrom
tomekzaw:measure-cache-unconstrained-fit-content

Conversation

@tomekzaw

@tomekzaw tomekzaw commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

computeFlexBasisForChild measures a child of a column container with the container's height as a FitContent available height. In a scroll view's content that height is the height of all the content, so it changes whenever a row is added. Measurements of nodes without a measure function are only reused when the available size matches exactly, so every clean child misses the cache and is measured again, together with its subtree.

This reuses a cached FitContent measurement when the measured height plus margin stayed below the old available height (the constraint didn't apply) and still fits the new one. A node that fills its available height, for example a container that stretches under the StretchFlexBasis errata, ends up exactly at the old available height and is not reused.

A percentage resolved against the available height breaks that assumption: an auto-height container with a height: 50% child measures 500 with 1000 available and 600 with 1200, without filling either. Each node therefore records whether its last layout resolved a height, min/max height or flex basis percentage that reaches its available height, and such nodes are not reused. Percentages below a node with a definite height (in points, or from an aspect ratio and a defined width) resolve against that height, and absolutely positioned children don't affect the container's size, so neither counts.

Like the other cache lookups since #2042, the reuse also requires the owner size to match when the node's style resolves a percentage against it (percentage padding, margin, width or min/max width). Otherwise a node with a fixed width keeps its available width when its owner gets narrower, and would reuse a measurement whose percentages were resolved against the old width.

Metrics

React Native 0.86 release build of a production app on a Galaxy A16, Perfetto, 4 interleaved runs per variant, each 15 flings through a long feed of text, image and video rows. Per React commit that adds feed rows:

Profile feed before after
nodes laid out per commit 864 200
layout per commit 7.7 ms 1.9 ms
whole commit 10.5 ms 3.9 ms
JS thread CPU during the scroll 6.29 s 5.70 s
Home timeline before after
nodes laid out per commit 1,219 156
layout per commit 13.3 ms 3.3 ms
whole commit 17.0 ms 5.2 ms
JS thread CPU during the scroll 7.22 s 6.46 s

Not reusing nodes that resolve a height percentage keeps nearly all of the gain: without that check, the commits took 3.9 ms and 5.0 ms.

Test plan

  • New tests:
    • unconstrained_fit_content_height_measure_is_reused: fails on main (one extra measure).
    • percent_height_in_auto_container_relayout_on_root_height_change: the case from review; fails without the percentage check (the container stays at 500 instead of 600).
    • percent_height_under_definite_height_measure_is_reused and absolute_percent_height_child_measure_is_reused: percentages below a fixed-size or aspect-ratio box, or in an absolutely positioned child, still allow the reuse.
    • percent_padding_follows_owner_size_on_relayout_with_taller_fit_content: a percentage padding under an owner that gets narrower and taller; fails without the owner size check (the child stays 60 tall instead of 40).
  • All 869 tests pass on the branch merged with current main (Debug build, CMake).
  • The two checks below, and the metrics above, were run before merging Key cached layouts on the owner size a node's percentages resolve against #2042 into this branch.
  • A randomized check, not part of this PR: 4,000 random trees with percentage, fixed and aspect-ratio heights, absolute and display: none children, wrapping and min/max sizes, each laid out again at 5 root heights. The layouts are identical to main in all 20,000 cases. The first version of this PR differed in 8,088 of them.
  • In the app, a verify mode that ran the full measurement anyway and compared it with the cached one: 129,000 reuse candidates while scrolling both feeds and opening a profile, 0 differences in measured size.

🤖 Generated with Claude Code

A child of a column container is measured with the container's height as a
FitContent available height. In a scroll view's content that height grows
with the content, so every clean child missed the measurement cache (which
matched available sizes exactly) whenever a row was added.

Reuse a cached FitContent measurement when it did not fill its available
height and still fits the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed label Oct 16, 2026
@tomekzaw
tomekzaw marked this pull request as ready for review October 16, 2026 09:13
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 16, 2026
@intergalacticspacehighway

Copy link
Copy Markdown
Collaborator

Do percent heights still work with this? For example - root (width 400, height 1000) -> container (auto) -> child (height: 50%). If we change the root height to 1200 and lay out again, i think the container will be cached to 500 instead of 600. Maybe if the feed is using a virtualised list this won't be that big of an issue? 😅

@tomekzaw

Copy link
Copy Markdown
Contributor Author

@intergalacticspacehighway Good catch, I will check that and will let you know

tomekzaw and others added 2 commits October 16, 2026 18:57
A height, min/max height or flex basis in percent anywhere in the subtree
is resolved against the available height, so a measurement that didn't
fill its available height can still change when that height does (e.g. a
50% child in an auto-height container). Track per node whether its last
layout resolved such a percentage, and only reuse an unconstrained
FitContent measurement when it didn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A percentage below a node with a definite height (in points, or from an
aspect ratio and a defined width) resolves against that height, and an
absolutely positioned child doesn't affect its container's size, so
neither makes a measurement depend on the available height. Stop the
flag at such nodes, so containers with e.g. a 100% child inside a
fixed-size or aspect-ratio box keep reusing their measurement.

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

Copy link
Copy Markdown
Contributor Author

@intergalacticspacehighway I added your case as a test and it failed indeed: the container stayed at 500 instead of 600. A virtualized list wouldn't have helped much here, since it affects any auto-height container with a percent-height descendant, e.g. after a rotation or a keyboard resize.

Fixed in a852987 and 748729f: each node now records whether its last layout resolved a height, min/max height or flex basis percentage against its available height, and such nodes don't reuse the measurement. Percentages below a node with a definite height (points, or aspect ratio and width) and inside absolutely positioned children don't count, since they can't change the measured size, so the usual height: '100%' inside a fixed-size or aspect-ratio box still gets the speedup.

Claude also ran 4,000 random trees, each laid out again at 5 root heights: the layouts now match main in all 20,000 cases (the previous version differed in 8,088). The speedup on device is about the same; the updated numbers are in the description.

tomekzaw and others added 2 commits October 19, 2026 12:40
Resolves the conflict in tests/YGRelayoutTest.cpp by keeping the tests from both sides.

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

The lookup for unconstrained FitContent measurements skipped the owner size
check that react#2042 added to the other cache lookups. A clean node with a fixed
width and percentage padding, margin or width keeps the same available width
when its owner's width changes, so a taller owner made it reuse a measurement
whose percentages were resolved against the previous width.

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

Copy link
Copy Markdown
Collaborator

Thanks for updating! There are still more cases though. e.g. percent gaps. root (w400, h1000) -> container (auto, rowGap: 10%) -> two 50pt children. Relayout at h1200 gives 120 instead of 122. I think instead of listing the cases that break it, we could flip the check? only reuse when the whole subtree uses values we know are safe (auto/point heights, no percent gaps, no negative margins, etc.), otherwise skip. Then a missed case is just slower, not wrong? 🤔

Rough AI untested solution.

Also btw, by virtualised list I meant the general performance problem in the description, not the percent case. In feed-like layouts with a virtualised list/legendlist, would the optimisation help performance wise? just trying to understand 😅

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 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