react-native-skia icon indicating copy to clipboard operation
react-native-skia copied to clipboard

Performance tanks when using animated transform property on Group

Open kkx64 opened this issue 4 months ago • 2 comments

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
  );
});

kkx64 avatar Aug 31 '25 13:08 kkx64

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.

AdamGerthel avatar Sep 05 '25 11:09 AdamGerthel

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.

wcandillon avatar Sep 05 '25 12:09 wcandillon