Skip to main content

Performance

Animation Smoothness on Low-End Devices

Complex animations with many interpolations may not run at consistent 60fps on older devices. Solution: Test on actual low-end devices, simplify animations, or use conditional logic:
import { Platform } from 'react-native';

const interpolator = ({ progress }) => {
  "worklet";

  // Simpler animation on Android
  if (Platform.OS === 'android') {
    return {
      content: {
        style: {
          opacity: interpolate(progress, [0, 1], [0, 1]),
        },
      },
    };
  }

  // Complex animation on iOS
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
        transform: [
          { translateY: interpolate(progress, [0, 1], [100, 0]) },
          { scale: interpolate(progress, [0, 1], [0.9, 1]) },
        ],
      },
    },
  };
};

Shader Compilation

Complex transforms compile shaders on first run, causing a brief stutter. Solution: Profile early, test on target devices, keep transforms simple:
// ✅ Simple transforms compile quickly
transform: [{ translateY: 0 }]

// ⚠️ Complex transforms trigger recompilation
transform: [
  { perspective: 1000 },
  { rotateX: '45deg' },
  { skewY: '10deg' },
]

Memory Usage with Bounds Tracking

Tracking many shared elements increases memory footprint. Solution: Use only necessary shared elements, clean up unused bounds:
// ✅ Track only visible elements
const visibleBounds = bounds({ id: 'visible-element' });

// ❌ Avoid tracking off-screen elements
const allBounds = allElements.map(e => bounds({ id: e.id }));

Gesture Interaction

Simultaneous Gesture Conflicts

When ScrollView and navigator gestures respond to the same input, behavior may be unpredictable. Solution: Use sheetScrollGestureBehavior to define boundaries:
<Transition.Modal
  snapPoints={[0.5, 1]}
  // Define clear boundary behavior
  sheetScrollGestureBehavior="expand-and-collapse"
  gestureEnabled
>
  <Transition.ScrollView>
    {/* ScrollView controls scroll, sheet controls expand/collapse */}
  </Transition.ScrollView>
</Transition.Modal>

Gesture Ownership Shadowing

A child navigator’s gesture claims the same axis as a parent, preventing parent interaction. Solution: Use different gesture directions or disable child gesture:
// ✅ Different axes: no conflict
<Transition.Stack gestureDirection="horizontal">
  <Transition.Modal gestureDirection="vertical">
    {/* Both work simultaneously */}
  </Transition.Modal>
</Transition.Stack>

// ✅ Disable shadowing
<Transition.Modal gestureEnabled={false}>
  {/* Parent gesture still works */}
</Transition.Modal>
See Gesture Ownership for detailed rules.

Velocity Overshoot

High release velocity may cause animations to overshoot snap points. Solution: Cap velocity or reduce velocity impact:
<Transition.Modal
  snapPoints={[0.5, 1]}
  gestureReleaseVelocityScale={0.5}  // Lower multiplier
  gestureReleaseVelocityMax={1000}   // Strict cap
  snapVelocityImpact={0.1}           // Reduce velocity influence
  gestureEnabled
>
  {/* Snap points more stable */}
</Transition.Modal>

Shared Elements

Bounds Measurement Delays

Shared element bounds may not be immediately available when animation starts. Ensure shared element boundaries are mounted and visible before triggering navigation to guarantee measurement timing.

Shape Mismatch Between Source and Destination

If source and destination have different aspect ratios, animation may look stretched. Solution: Ensure consistent dimensions or use letterboxing:
// ✅ Consistent aspect ratio
// Source: 100x100 square
<Transition.Boundary.Pressable id="avatar">
  <Image style={{ width: 100, height: 100 }} />
</Transition.Boundary.Pressable>

// Destination: 200x200 square (same ratio)
<Transition.Boundary.View id="avatar">
  <Image style={{ width: 200, height: 200 }} />
</Transition.Boundary.View>

// ❌ Mismatched aspect ratio stretches
// Source: 100x150 (portrait)
// Destination: 300x300 (square) — will stretch

Unmounted Boundary Components

If a shared element boundary unmounts before animation completes, bounds become stale. Solution: Keep boundary elements mounted during transitions:
// ✅ Boundary stays mounted
<Transition.Boundary.View id="avatar" style={{ display: mounted ? 'flex' : 'none' }}>
  <Image {...} />
</Transition.Boundary.View>

// ❌ Unmounting breaks bounds
{mounted && (
  <Transition.Boundary.View id="avatar">
    <Image {...} />
  </Transition.Boundary.View>
)}

Layout and Measurement

Dynamic Content Height

Content height may change during animation (e.g., images loading), affecting snap points. Solution: Pre-measure or use fixed heights:
// ✅ Pre-measure content
const contentHeight = useSharedValue(0);

<Transition.Modal
  snapPoints={[contentHeight, 1]}
>
  <View onLayout={(e) => { contentHeight.value = e.nativeEvent.layout.height }}>
    {/* Content */}
  </View>
</Transition.Modal>

// ✅ Auto snap points adjust dynamically
<Transition.Modal snapPoints={["auto", 1]}>
  {/* Height measured automatically */}
</Transition.Modal>

Safe Area Insets Not Applied

Content may overflow into notches or safe areas. Solution: Manually apply safe area insets:
import { useSafeAreaInsets } from 'react-native-safe-area-context';

function MyScreen() {
  const insets = useSafeAreaInsets();

  return (
    <View style={{
      flex: 1,
      paddingTop: insets.top,
      paddingBottom: insets.bottom,
    }}>
      {/* Content respects safe areas */}
    </View>
  );
}

Back Button Behavior

Android back button behavior may differ from iOS swipe gesture. Solution: Test both platforms, provide visual back affordance:
import { useNavigation } from '@react-navigation/native';

function MyScreen() {
  const navigation = useNavigation();

  useEffect(() => {
    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
      // Custom back button handling
    });
    return unsubscribe;
  }, [navigation]);

  return <View>{/* Content */}</View>;
}
Deep links may not trigger animations if navigators aren’t set up for it. Solution: Ensure navigators are initialized before deep linking:
// In app.json or linking config
const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Home: '/',
      Details: '/details/:id',
    },
  },
};

// Navigators properly initialized for deep links
<NavigationContainer linking={linking}>
  <RootStack />
</NavigationContainer>

Platform-Specific Issues

iOS vs Android Differences

Gesture and animation behavior differs slightly:
AspectiOSAndroid
Swipe gestureSmooth, preciseMay feel less responsive
Bounce/overshootNatural bounceRequires config
Header animationsSmoothMay stutter
Safe areaAuto-handledManual insets needed
Solution: Test on both platforms, use platform-specific conditionals when needed:
import { Platform } from 'react-native';

const gestureDirection = Platform.OS === 'ios' ? 'horizontal' : 'vertical';

Notch/Safe Area

Notched devices need explicit handling. Solution: Use react-native-safe-area-context:
import { SafeAreaProvider } from 'react-native-safe-area-context';

<SafeAreaProvider>
  <NavigationContainer>
    <RootStack />
  </NavigationContainer>
</SafeAreaProvider>

Floating Action Button (FAB)

FAB may conflict with gesture detection or snap points. Solution: Position FAB outside gesture area or disable during animation:
const [animating, setAnimating] = useSharedValue(false);

<View style={{ flex: 1 }}>
  <Transition.Stack screenStyleInterpolator={interpolator}>
    {/* screens */}
  </Transition.Stack>

  <Pressable
    style={{
      position: 'absolute',
      bottom: 20,
      right: 20,
      pointerEvents: animating ? 'none' : 'auto',
    }}
  >
    <FAB />
  </Pressable>
</View>

Experimental Features

experimental_animateOnInitialMount

This feature is experimental and may change or be removed. Status: Available in v3.4 but subject to API changes. Solution: Use with caution, test thoroughly:
<Transition.Stack
  experimental_animateOnInitialMount
  screenStyleInterpolator={interpolator}
>
  {/* Animations run on first mount */}
</Transition.Stack>
Not recommended for production use without thorough testing.

Compatibility

React Navigation Version

Requires React Navigation 6.0+. Earlier versions not supported. Check version:
npm list react-navigation
# Should be 6.0.0 or higher

Reanimated Version

Requires Reanimated 3.0+. Version 2 not supported. Check version:
npm list react-native-reanimated
# Should be 3.0.0 or higher

Best Practices

  1. Profile early: Test animations on target devices
  2. Limit complexity: Simpler animations are more reliable
  3. Test both platforms: iOS and Android have different behaviors
  4. Use TypeScript: Catch type errors early
  5. Handle edge cases: Unmounting, interruptions, etc.
  6. Monitor performance: Use React DevTools Profiler
  7. Test low-end devices: Don’t assume powerful hardware

Getting Help

If you encounter issues:
  1. Check troubleshooting sections in guides
  2. Review gesture ownership rules
  3. Test with simplified interpolator
  4. Check GitHub issues
  5. Read Reanimated docs

Summary

The library is powerful but has edge cases. Understanding these caveats helps you build robust, performant applications. When in doubt, simplify and test on real devices.

Next Steps