Repository navigation
Skip layer invalidation when an iOS View only changes position - #59168
Open
hryhoriiK97 wants to merge 1 commit into
Open
hryhoriiK97 wants to merge 1 commit into
hryhoriiK97 wants to merge 1 commit into
Conversation
`RCTViewComponentView` requested a full `invalidateLayer` on every layout metrics update, including updates where only the frame origin changed. Everything the invalidation derives is in the view's own coordinates, so a view that merely moved rebuilt its shadow path, border rendering and background, box-shadow and mask layers for nothing. The hover shape is the exception, since iOS evaluates it in the superview's space, so views with `cursor: pointer` keep invalidating on moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
RCTViewComponentView(React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm) applies a view's borders, shadow path, background colour and image layers, box shadows, outline, filters and clipping masks ininvalidateLayer, which runs fromfinalizeUpdates:whenever_needsInvalidateLayeris set.Current behavior.
updateLayoutMetrics:oldLayoutMetrics:sets_needsInvalidateLayerunconditionally. The mounting layer calls it whenever any layout metric differs, including the frame origin alone, so a view that merely moved runs the whole invalidation: it resolves border metrics, recreates the shadow path, re-applies about ten layer properties, re-rasterizes the border image when borders are non-uniform, and removes and recreates the box-shadow, outline, background-image and mask sublayers. When the content above a list changes height, every card below it pays this although nothing it draws has changed.With this change. The layout metrics are compared with the stored ones with the origin masked out. If only the origin changed, the invalidation is not requested; every other change (size, content insets, border widths, overflow inset, display type, layout direction, scale factor) still invalidates as before, and so does a recycled view, whose stored metrics are
EmptyLayoutMetrics. EverythinginvalidateLayerderives is in the view's own coordinate space (bounds,layer.bounds, padding frame), with one exception: the iOS 17 hover shape is built fromself.frameto work around a UIKit bug, so views withcursor: pointerkeep invalidating on moves. The subview and sublayer frame updates inupdateLayoutMetrics:are unchanged.Why. Position-only layout updates are the common case in a reflow: inserting a row, expanding an accordion, a keyboard-avoiding shift, or a LayoutAnimation moving content. The cost scales with the number of styled views below the change.
Changelog:
[IOS] [FIXED] - Views no longer rebuild their layer styling (borders, shadows, background and mask layers) when only their position changes
Test Plan:
node scripts/clang-format.js --checkon the changed file passes.xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1).boxShadow, the list area is byte-identical before and after.ScrollView, each aViewwrapping aText; a "reflow" toggles a spacer above the list between 0 and 40pt, so every card moves and none changes size. Three contents: plain (unstyled wrapper, which Fabric flattens, so only theTextviews remain), card (borderRadius: 8,borderWidth: 1, solidbackgroundColor,shadowColor/Opacity/Radius/Offset) and the same card withboxShadowinstead. Timed from Fabric's transaction telemetry (mount phase), a timer aroundinvalidateLayer, a run-loop observer after the CoreAnimation commit (mount end to display), and thetouchendEvent Timing entry for the press (press to mount end). The two arms are the same binary with the new check switched by an environment variable; both include Skip redrawing iOS Text when only its position changes #59155 so that text redraws do not dominate the reflow. The presses were driven by Argent on the physical iPhone, with no accessibility polling between presses. iPhone 15 Pro, Release, medians of 5 reflows:invalidateLayertotal (main thread)boxShadow)invalidateLayertotal (main thread)Each arm was run twice. The repeat of the before arm gave 114.1 ms and 132.1 ms press to mount end for the two card styles (58.2 ms and 66.9 ms of invalidation), the repeat of the after arm 52.3 ms and 53.2 ms (1.1 ms and 0.9 ms), so the saving is 39 to 68% of the press across both runs and both card styles. The remaining invalidations are views whose size or props do change in that commit, such as the harness's own status labels. The plain list is unchanged (0.8 ms of invalidation per reflow before, under 0.1 ms after, press to mount end within noise at 41 and 46 ms), as are mounting and unmounting: mounting the 1000 cards measures 141 ms in the mount phase in both arms, with the same 2001 invalidations.
Not exercised: the hover shape for
cursor: pointer, which needs a pointer device; by design those views invalidate on moves exactly as before.🤖 Generated with Claude Code