Skip to content

Let Skia cache the mask of a blurred Android inset box shadow - #59083

Open
YevheniiKotyrlo wants to merge 2 commits into
react:mainfrom
YevheniiKotyrlo:perf/android-inset-box-shadow-blur-mask-cache
Open

YevheniiKotyrlo wants to merge 2 commits into
react:mainfrom
YevheniiKotyrlo:perf/android-inset-box-shadow-blur-mask-cache

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

InsetBoxShadowDrawable draws its shadow with drawDoubleRoundRect. With a blur, Skia's GPU backend turns a double round rect into a new volatile even-odd path on every draw, and it caches a mask-filtered mask only for a shape it can key, so a blurred inset shadow renders its mask offscreen, and blurs it, every time the view draws.

A blurred inset shadow now draws an even-odd path the drawable builds with the rest of its geometry, so Skia keys the blurred mask on the path, the blur and the matrix, and draws it from its cache on the next frame. An unblurred inset shadow keeps drawDoubleRoundRect, which Skia draws analytically.

This builds on the cached geometry from #59081 and includes its commit.

Changelog:

[ANDROID] [FIXED] - Blurred inset box shadows no longer render their mask offscreen on every draw

Test Plan:

BoxShadowDrawableTest gains five cases: a blurred rounded and a blurred rectangular inset shadow hand the canvas one even-odd Path, the same instance on every draw (on #59081 they draw no path at all); an unblurred inset shadow still draws a double round rect and no path; and a blurred inset shadow follows bounds growing from empty and a radius changed in place and reassigned, compared with a freshly built one.

  • ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true: 781 tests, 0 failures
  • node ./scripts/format-kotlin.js --check: clean

On device (evidence), RNTester release builds of #59081 and of this branch, 150 views with an animated background and inset 0 2px 6px: Skia's render passes per frame fall from 530 to 37 on a Galaxy S23 (Android 14, API 34, Adreno 740), and the median frame from 21.0 to 13.6 ms there and from 395.6 to 19.5 ms on a Mi MIX 3 (Android 10, API 29, Adreno 630). The no-shadow control stays level, and the static frame differs only inside the blur, by at most 4 of 255 per channel (the before/after pair is in the same comment).

I didn't run the full Android build, the template and HelloWorld app builds, or the Android e2e jobs locally.

@YevheniiKotyrlo

YevheniiKotyrlo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Device measurement — before / after

#59081 alone — every frame of an animated background renders each blurred inset shadow's mask offscreen and blurs it: 530 render passes per frame for 150 views on a Galaxy S23.

This PR — 37: Skia draws all but seven of the masks from its cache each frame.

#59081 this PR
Galaxy S23 (Android 14, API 34, Adreno 740) — frame 21.0 ms 13.6 ms
Galaxy S23 — Skia render passes per frame 530 37
Mi MIX 3 (Android 10, API 29, Adreno 630) — frame 395.6 ms 19.5 ms
control: no shadow, S23 / Mi MIX 3 8.9 / 18.5 ms 8.9 / 19.0 ms

RNTester release builds for arm64-v8a of #59081's head and of this branch, median of five 6 s dumpsys gfxinfo framestats windows per arm; render passes are Ganesh OpsTask executions in a 3 s Perfetto capture with debug.hwui.skia_atrace_enabled. The static frame differs only inside the shadows' blur, by at most 4 of 255 per channel on the S23 and 1 on the Mi MIX 3.

Scene

The BoxShadowStress scene from #59081's measurement, with boxShadow set to 'inset 0 2px 6px #7c3aed99', or null for the control.

Device evidence — before / after

#59081 alone — a blurred inset shadow drawn as a double round rect.

This PR — the same shadow drawn from its cached even-odd path: the pixels that differ all lie inside the blur, by at most 4 of 255 per channel.

before — #59081 after — this PR

Galaxy S23 (Android 14, API 34), 1080×2340 at density 480, the scene above with inset 0 2px 6px #7c3aed99 and no animation, cropped to 16 of the 150 views. Below the status bar, 553,203 pixels of the full frame differ, by at most 4 of 255; on the Mi MIX 3, 44,368 differ, by at most 1.

@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 14, 2026
InsetBoxShadowDrawable and OutsetBoxShadowDrawable resolved the border
radii and built new RectF, Path and radii arrays inside draw(), so every
redraw of a view's background, every frame of an animation that touches
it, allocated them again and handed the canvas a new Path. Both drawables
now keep that geometry in fields and rebuild it only when its inputs
change: on a bounds or layout direction change, and when borderRadius or
borderInsets is assigned. An alpha or color filter change only redraws.
A blurred inset shadow is drawn with drawDoubleRoundRect, and Skia turns a
double round rect with a mask filter into a new volatile path on every draw,
which it never caches: the blurred mask is rendered again, offscreen, every
time the view is drawn.

Draw a blurred inset shadow from a cached even-odd path instead, so Skia can
key its blurred mask on the path, the blur and the matrix. An unblurred shadow
keeps drawDoubleRoundRect, which Skia draws analytically.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the perf/android-inset-box-shadow-blur-mask-cache branch from 5d65b93 to e32e6b2 Compare October 18, 2026 22:50
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 18, 2026 23:04
@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 18, 2026

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

1 participant