Repository navigation
Stop rebuilding Android box-shadow geometry on every draw - #59081
YevheniiKotyrlo wants to merge 1 commit into
Conversation
Device measurement — before / after
This PR — 2.9 ms and 3.0 ms. The render thread, the no-shadow control and the rendered pixels are unchanged.
Mi MIX 3 (Android 10, API 29, Adreno 630, 60 Hz) and Galaxy S23 (Android 14, API 34, Adreno 740), RNTester release builds of Scenefunction BoxShadowStress({boxShadow}) {
const progress = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
const loop = Animated.loop(
Animated.timing(progress, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
}),
);
loop.start();
return () => loop.stop();
}, [progress]);
const backgroundColor = progress.interpolate({
inputRange: [0, 0.5, 1],
outputRange: ['#e5e7eb', '#ffffff', '#e5e7eb'],
});
return (
<View style={{flexDirection: 'row', flexWrap: 'wrap', gap: 12, padding: 8}}>
{Array.from({length: 150}, (_, index) => (
<Animated.View
key={index}
style={{width: 36, height: 36, borderRadius: 8, boxShadow, backgroundColor}}
/>
))}
</View>
);
}
|
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.
4b6e7fa to
ad615d5
Compare

Summary:
InsetBoxShadowDrawable.draw()andOutsetBoxShadowDrawable.draw()derive their clip and shadow paths, rects and resolved radii every time they run, anddraw()runs on every invalidation of the view's background. A background that animates therefore allocates a fresh set ofPath,RectF,FloatArrayandComputedBorderRadiusobjects per shadow per frame, on the UI thread, while it records the display list.Every one of those values depends only on the bounds, the border radius and insets, the layout direction and the constructor arguments. Both drawables now keep them in fields and rebuild them in
draw()only after one of those changed: theborderRadiusandborderInsetssetters,onBoundsChangeandonLayoutDirectionChangedmark them stale.BackgroundStyleApplicatormutates the sharedBorderRadiusStylein place and then reassigns it to each shadow, so the setters see every radius change. An alpha or color filter change only redraws, unlikeBackgroundDrawableandBorderDrawable, which mark their paths stale on everyinvalidateSelf().Changelog:
[ANDROID] [FIXED] - Box shadows no longer rebuild their paths and radii on every draw
Test Plan:
BoxShadowDrawableTest(new) draws each drawable twice through a recording canvas. Four cases assert that the second draw reuses the first draw'sPathandRectFinstances; onmainthey fail withExpecting actual: RectF(...) and actual: RectF(...) to refer to the same object. Seven compare a reused drawable with a freshly built one after a bounds change, a radius mutated in place and reassigned, new border insets, and a switch to RTL. Two check that an alpha and a color filter change resolve the border radius no second time../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true: 776 tests, 0 failuresnode ./scripts/format-kotlin.js --check: cleanRNTester release builds of
mainand of this branch, 150 views with a native-driven background and a 1px ring, on a Mi MIX 3 (Android 10, API 29): draw recording drops from 4.4 to 2.9 ms per frame forinset 0 0 0 1pxand from 5.2 to 3.0 ms for0 0 0 1px, with render-thread time and a no-shadow control unchanged and the static frames byte-identical. The chart and the scene are in the evidence comment.I didn't run the full Android
build, the template and HelloWorld app builds, or the Android e2e jobs locally.