Description
On the New Architecture, RCTScrollViewComponentView sizes its inner RCTEnhancedScrollView only via an autoresizing mask: the scroll view is created with initWithFrame:self.bounds — which is CGRectZero at component-view creation — and relies on UIViewAutoresizingFlexibleWidth | FlexibleHeight deltas to grow when the shell's frame is later applied (source).
Autoresizing only propagates deltas at the moment the parent's frame changes. Under heavy mount/teardown churn (reliably reproducible on a thermally-throttled device, where JS-driven mount choreography stretches from milliseconds to seconds), that delta moment can be missed. The inner UIScrollView is then left at CGRectZero permanently — there is no code path that ever re-establishes the invariant:
- the shell has its correct Yoga frame (
onLayout fires normally),
onContentSizeChange fires normally (state events come from the ShadowTree, not the native view),
- the content subtree is fully mounted and correctly laid out inside the scroll view,
- but everything is clipped into a zero-size viewport → content is invisible while the app believes everything is fine.
Captured native hierarchy of a live occurrence (Xcode view debugger, real device):
RCTScrollViewComponentView frame = (0, 429, 430, 503) ← correct Yoga frame
RCTEnhancedScrollView frame = (0, 0, 0, 0) ← never sized
UIView (container) frame = (0, 0, 430, 1024) ← matches contentSize
… full content tree, all frames correct, invisible …
To identify the writer we swizzled -[RCTEnhancedScrollView setFrame:] to veto+stack-trace any zero-size write: it never fires. Nothing writes a zero frame — the view is born at zero and the grow-delta is missed. This distinguishes the bug from external interference and points at the reliance on autoresizing itself.
Frequency in the wild: on a hot iPhone 15 Pro Max (thermal state "serious") this hit nearly every mount of screens whose ScrollView mounts one frame after the screen (skeleton→content swap), across multiple independent screens. It is also consistent with long-standing "ScrollView renders invisible after logout/onboarding transition" reports that apps (including ours) have historically dodged with full app reloads.
Related (but distinct, both recycling-related — recycling is off by default and was disabled in our repro): #55090, #34165.
Steps to reproduce
The failure is a race, so there is no minimal synchronous repro; the deterministic statement of the bug is:
RCTScrollViewComponentView is created (frame CGRectZero) → _scrollView created at zero with autoresizing W+H.
- If the shell's frame assignment lands in any state where autoresizing cannot propagate the delta to
_scrollView, the scroll view stays 0×0.
- No subsequent code (
updateLayoutMetrics, state updates, layoutSubviews) ever corrects it → permanent invisible content.
Practical reproduction (how we hit it ~every mount): iPhone at thermal state serious (charge + load), a screen that mounts its ScrollView one requestAnimationFrame after the screen mounts, navigate to the screen repeatedly. Verified on RN 0.83.6 / iOS 26.5 / New Architecture / Hermes; the relevant code is unchanged on main.
Expected / actual
- Expected: the inner scroll view always tracks the shell's bounds.
- Actual: it can be left at
CGRectZero forever; all content invisible; no error anywhere (JS, ShadowTree, or UIKit).
Fix
PR attached: enforce the invariant explicitly — re-glue _scrollView to the shell's bounds in updateLayoutMetrics and layoutSubviews (bounds.size + center rather than frame, so the RTL transform is respected and bounds.origin = contentOffset is preserved). Verified on-device: the desync still occurs under the same conditions and now heals within one layout pass; the invisible-content states no longer reproduce.
Description
On the New Architecture,
RCTScrollViewComponentViewsizes its innerRCTEnhancedScrollViewonly via an autoresizing mask: the scroll view is created withinitWithFrame:self.bounds— which isCGRectZeroat component-view creation — and relies onUIViewAutoresizingFlexibleWidth | FlexibleHeightdeltas to grow when the shell's frame is later applied (source).Autoresizing only propagates deltas at the moment the parent's frame changes. Under heavy mount/teardown churn (reliably reproducible on a thermally-throttled device, where JS-driven mount choreography stretches from milliseconds to seconds), that delta moment can be missed. The inner
UIScrollViewis then left atCGRectZeropermanently — there is no code path that ever re-establishes the invariant:onLayoutfires normally),onContentSizeChangefires normally (state events come from the ShadowTree, not the native view),Captured native hierarchy of a live occurrence (Xcode view debugger, real device):
To identify the writer we swizzled
-[RCTEnhancedScrollView setFrame:]to veto+stack-trace any zero-size write: it never fires. Nothing writes a zero frame — the view is born at zero and the grow-delta is missed. This distinguishes the bug from external interference and points at the reliance on autoresizing itself.Frequency in the wild: on a hot iPhone 15 Pro Max (thermal state "serious") this hit nearly every mount of screens whose ScrollView mounts one frame after the screen (skeleton→content swap), across multiple independent screens. It is also consistent with long-standing "ScrollView renders invisible after logout/onboarding transition" reports that apps (including ours) have historically dodged with full app reloads.
Related (but distinct, both recycling-related — recycling is off by default and was disabled in our repro): #55090, #34165.
Steps to reproduce
The failure is a race, so there is no minimal synchronous repro; the deterministic statement of the bug is:
RCTScrollViewComponentViewis created (frameCGRectZero) →_scrollViewcreated at zero with autoresizing W+H._scrollView, the scroll view stays 0×0.updateLayoutMetrics, state updates,layoutSubviews) ever corrects it → permanent invisible content.Practical reproduction (how we hit it ~every mount): iPhone at thermal state
serious(charge + load), a screen that mounts its ScrollView onerequestAnimationFrameafter the screen mounts, navigate to the screen repeatedly. Verified on RN 0.83.6 / iOS 26.5 / New Architecture / Hermes; the relevant code is unchanged onmain.Expected / actual
CGRectZeroforever; all content invisible; no error anywhere (JS, ShadowTree, or UIKit).Fix
PR attached: enforce the invariant explicitly — re-glue
_scrollViewto the shell's bounds inupdateLayoutMetricsandlayoutSubviews(bounds.size + center rather than frame, so the RTL transform is respected andbounds.origin= contentOffset is preserved). Verified on-device: the desync still occurs under the same conditions and now heals within one layout pass; the invisible-content states no longer reproduce.