Performance tanks when using animated transform property on Group
Description
When using a <Group /> with an animated transform prop (Reanimated, useSharedValue, useDerivedValue) the performance tanks very noticeably, especially apparent on Android.
React Native Skia Version
1.5.0
React Native Version
0.76.9
Using New Architecture
- [ ] Enabled
Steps to Reproduce
You can try adding some images to a Group in a Canvas, and use a constantly changing value to animate its transform (e.g. useDerivedValue with a Gyroscope sensor). Especially apparent as you add more canvases in view.
Snack, Code Example, Screenshot, or Link to Repository
import React, { useEffect, useMemo } from 'react';
import {
withSpring,
useSharedValue,
useDerivedValue,
} from 'react-native-reanimated';
import Color from 'color';
import lerp from '../../utils/lerp';
import { Gyroscope, GyroscopeMeasurement } from 'expo-sensors';
import {
Canvas,
Group,
Size,
Image,
Blur,
ColorMatrix,
useImage,
} from '@shopify/react-native-skia';
import { Platform } from 'react-native';
import _ from 'lodash';
const reflectionImage = require('../../assets/images/garage-reflection.png');
const CANVAS_STYLE = {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
};
const SPRING_CONFIG = {
mass: Platform.select({ ios: 0.12, android: 0.5 }),
damping: 20,
stiffness: 120,
restDisplacementThreshold: 0.1,
restSpeedThreshold: 0.1,
};
const REFLECTION_IMAGE_SIZE = 512;
interface ReflectiveLayerProps {
smoothness?: number;
intensity?: number;
tintColor?: string;
rotation?: number;
}
const ReflectiveLayer: React.FC<ReflectiveLayerProps> = ({
smoothness = 0.5,
intensity = 0.7,
rotation = 0,
tintColor,
}) => {
const gyroscopeRoll = useSharedValue(0);
const gyroscopePitch = useSharedValue(0);
const garageImg = useImage(reflectionImage);
const canvasSize = useSharedValue<Size>({ width: 0, height: 0 });
useEffect(() => {
Gyroscope.setUpdateInterval(66);
const handleGyroscopeData = (data: GyroscopeMeasurement) => {
const x = Math.abs(data.x) < 0.1 ? 0 : data.x;
const y = Math.abs(data.y) < 0.1 ? 0 : data.y;
const nextRoll = _.clamp(gyroscopeRoll.value + y * -20, -500, 125);
const nextPitch = _.clamp(gyroscopePitch.value + x * -20, -500, 125);
gyroscopeRoll.value = withSpring(nextRoll, SPRING_CONFIG);
gyroscopePitch.value = withSpring(nextPitch, SPRING_CONFIG);
};
const sub = Gyroscope.addListener(handleGyroscopeData);
return () => {
sub.remove();
};
}, [gyroscopeRoll, gyroscopePitch]);
const xTrans = useDerivedValue(() => {
const width = canvasSize.value.width;
const height = canvasSize.value.height;
const imgWidth = REFLECTION_IMAGE_SIZE;
const imgHeight = REFLECTION_IMAGE_SIZE;
// Center base position
const baseTx = (width - imgWidth) / 2;
const baseTy = (height - imgHeight) / 2;
const offsetX = (gyroscopeRoll.value - gyroscopePitch.value) * 0.5;
const offsetY = gyroscopePitch.value * 0.5;
let tx = baseTx + offsetX;
let ty = baseTy + offsetY;
const minTx = Math.min(0, width - imgWidth);
const maxTx = Math.max(0, width - imgWidth);
const minTy = Math.min(0, height - imgHeight);
const maxTy = Math.max(0, height - imgHeight);
if (tx < minTx) tx = minTx;
else if (tx > maxTx) tx = maxTx;
if (ty < minTy) ty = minTy;
else if (ty > maxTy) ty = maxTy;
return [{ translateX: tx }, { translateY: ty }, { rotateZ: rotation }];
});
const tintMatrix = useMemo(() => {
if (!tintColor) return null;
const c = Color(tintColor).lighten(0.4).rgb();
const r = c.red() / 255;
const g = c.green() / 255;
const b = c.blue() / 255;
return [r, 0, 0, 0, 0, 0, g, 0, 0, 0, 0, 0, b, 0, 0, 0, 0, 0, 1, 0];
}, [tintColor, intensity]);
const blurSoft = useMemo(
() => _.clamp((1 - smoothness + 0.01) * 16, 1, 12),
[smoothness],
);
const blurBright = useMemo(
() => _.clamp((1 - smoothness + 1.0) * 10, 2, 16),
[smoothness],
);
return (
<Canvas
style={CANVAS_STYLE}
pointerEvents="none"
onSize={canvasSize}
removeClippedSubviews
renderToHardwareTextureAndroid
>
<Group transform={xTrans} layer>
<Image
opacity={lerp(0, 0.5, intensity)}
image={garageImg}
fit="cover"
width={REFLECTION_IMAGE_SIZE}
height={REFLECTION_IMAGE_SIZE}
>
<Blur blur={blurSoft} />
{tintMatrix && <ColorMatrix matrix={tintMatrix} />}
</Image>
<Image
opacity={lerp(0, 0.85, intensity)}
image={garageImg}
fit="cover"
width={REFLECTION_IMAGE_SIZE}
height={REFLECTION_IMAGE_SIZE}
blendMode="plus"
>
<Blur blur={blurBright} />
{tintMatrix && <ColorMatrix matrix={tintMatrix} />}
</Image>
</Group>
</Canvas>
);
};
export default React.memo(ReflectiveLayer, (prevProps, nextProps) => {
return (
prevProps.tintColor === nextProps.tintColor &&
prevProps.smoothness === nextProps.smoothness &&
prevProps.intensity === nextProps.intensity &&
prevProps.rotation === nextProps.rotation
);
});
I can't say with certainty that it's the same thing, but we're seeing intermittent FPS drops on a group transform that's based ona a derived value. When switching between two values multiple times, every 5th time or so will cause major lag. I've mostly seen it on iOS.
The bug report mentions RN Skia 1.5.0, are you experiencing the same behavior on the latest version? If yes, I would be interested to take a look.
Kind regards,
William
On Fri, Sep 5, 2025 at 1:55 PM Adam Gerthel @.***> wrote:
I can't say with certainty that it's the same thing, but we're seeing intermittent FPS drops on a group transform that's based ona a derived value. When switching between two values multiple times, every 5th time or so will cause major lag. I've mostly seen it on iOS.
— Reply to this email directly, view it on GitHub or unsubscribe. You are receiving this email because you are subscribed to this thread.
Triage notifications on the go with GitHub Mobile for iOS or Android.