Back to Blog
Mobile Development
Advanced Usage of Worklets for High-Performance React Native Applications
1/18/2025
10 min read
By X Software Team
Advanced Usage of Worklets for High-Performance React Native Applications

Advanced Usage of Worklets for High-Performance React Native Applications

Worklets have revolutionized how we build performant, responsive UIs in React Native. By running JavaScript code on the UI thread instead of the JS thread, worklets enable butter-smooth animations and instant gesture responses that were previously impossible. In this comprehensive guide, we'll explore advanced worklet techniques that will take your app's performance to the next level.

Understanding Worklets

What Are Worklets?

Worklets are special JavaScript functions that can run on the UI thread. Unlike regular JavaScript code that runs on the JS thread, worklets execute directly on the UI thread, giving you:

  • 60 FPS animations even when JS thread is busy
  • Instant gesture responses with zero lag
  • Smooth transitions that never drop frames
  • Predictable performance across all devices
'worklet'; // This directive marks a function as a worklet

function myWorklet(value) {
  'worklet';
  return value * 2;
}

The React Native Thread Architecture

┌─────────────────┐
│   JS Thread     │ ← Regular JavaScript execution
│  (Metro/Hermes) │
└────────┬────────┘
         │ Bridge
┌────────▼────────┐
│   UI Thread     │ ← Worklets run here!
│    (Native)     │
└─────────────────┘

When you run code on the JS thread, it must communicate with the UI thread via the bridge. This creates lag. Worklets bypass this completely.

Setting Up Worklets

Installation

# Install react-native-reanimated
npm install react-native-reanimated

# iOS setup
cd ios && pod install

# Android setup is automatic with autolinking

Babel Configuration

// babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    'react-native-reanimated/plugin', // Must be last!
  ],
};

Basic Worklet Concepts

Shared Values

Shared values are special values that can be accessed from both threads.

import { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';

function AnimatedComponent() {
  // Create a shared value
  const offset = useSharedValue(0);

  // Use in animated style (runs as worklet)
  const animatedStyles = useAnimatedStyle(() => {
    return {
      transform: [{ translateX: offset.value }],
    };
  });

  return <Animated.View style={animatedStyles} />;
}

The 'worklet' Directive

// ❌ This won't work - not marked as worklet
const myFunction = (value) => {
  return value * 2;
};

// ✅ This works - properly marked
const myWorklet = (value) => {
  'worklet';
  return value * 2;
};

// ✅ Auto-detected in useAnimatedStyle
const animatedStyles = useAnimatedStyle(() => {
  // Automatically a worklet, no directive needed
  return { opacity: offset.value };
});

Advanced Worklet Patterns

1. Complex Gesture Handling

Create responsive, lag-free gestures using worklets.

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  runOnJS,
} from 'react-native-reanimated';

function DraggableCard({ onSwipeComplete }) {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const startX = useSharedValue(0);
  const startY = useSharedValue(0);

  // Complex gesture logic runs entirely on UI thread
  const gesture = Gesture.Pan()
    .onBegin(() => {
      'worklet';
      startX.value = translateX.value;
      startY.value = translateY.value;
    })
    .onUpdate((event) => {
      'worklet';
      translateX.value = startX.value + event.translationX;
      translateY.value = startY.value + event.translationY;

      // Complex calculations on UI thread
      const distance = Math.sqrt(
        translateX.value ** 2 + translateY.value ** 2
      );

      // Threshold-based feedback
      if (distance > 200) {
        // Haptic feedback could go here
      }
    })
    .onEnd(() => {
      'worklet';
      const shouldSwipe = Math.abs(translateX.value) > 150;

      if (shouldSwipe) {
        // Animate off screen
        translateX.value = withSpring(
          translateX.value > 0 ? 500 : -500,
          { velocity: 2000 },
          () => {
            // Callback runs on UI thread, need to switch to JS
            runOnJS(onSwipeComplete)();
          }
        );
      } else {
        // Spring back to center
        translateX.value = withSpring(0);
        translateY.value = withSpring(0);
      }
    });

  const animatedStyle = useAnimatedStyle(() => {
    // Interpolate rotation based on horizontal position
    const rotation = (translateX.value / 500) * 30; // 30 degrees max

    return {
      transform: [
        { translateX: translateX.value },
        { translateY: translateY.value },
        { rotate: `${rotation}deg` },
      ],
    };
  });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Text>Swipe me!</Text>
      </Animated.View>
    </GestureDetector>
  );
}

2. Scroll-Synchronized Animations

Create header animations that respond to scroll position.

import { useAnimatedScrollHandler, interpolate, Extrapolate } from 'react-native-reanimated';

function ParallaxHeader() {
  const scrollY = useSharedValue(0);

  // Scroll handler runs as worklet
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollY.value = event.contentOffset.y;
    },
  });

  // Header animation
  const headerStyle = useAnimatedStyle(() => {
    const height = interpolate(
      scrollY.value,
      [0, 200],
      [300, 100],
      Extrapolate.CLAMP
    );

    const opacity = interpolate(
      scrollY.value,
      [0, 100],
      [1, 0],
      Extrapolate.CLAMP
    );

    return {
      height,
      opacity,
    };
  });

  // Title animation
  const titleStyle = useAnimatedStyle(() => {
    const scale = interpolate(
      scrollY.value,
      [0, 200],
      [1, 0.8],
      Extrapolate.CLAMP
    );

    const translateY = interpolate(
      scrollY.value,
      [0, 200],
      [0, -20],
      Extrapolate.CLAMP
    );

    return {
      transform: [
        { scale },
        { translateY },
      ],
    };
  });

  return (
    <>
      <Animated.View style={[styles.header, headerStyle]}>
        <Animated.Text style={[styles.title, titleStyle]}>
          My App
        </Animated.Text>
      </Animated.View>

      <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
        {/* Content */}
      </Animated.ScrollView>
    </>
  );
}

3. Advanced Spring Animations

Create physics-based animations with custom spring configurations.

import { withSpring, withTiming, withDecay } from 'react-native-reanimated';

function PhysicsButton() {
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);

  const handlePress = () => {
    'worklet';

    // Custom spring config for bouncy effect
    scale.value = withSpring(
      0.95,
      {
        mass: 1,
        damping: 15,
        stiffness: 150,
        overshootClamping: false,
        restDisplacementThreshold: 0.01,
        restSpeedThreshold: 0.01,
      },
      () => {
        // Chain animations
        scale.value = withSpring(1);
      }
    );

    // Simultaneous rotation
    rotation.value = withSpring(rotation.value + 360, {
      damping: 20,
      stiffness: 90,
    });
  };

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { scale: scale.value },
        { rotate: `${rotation.value}deg` },
      ],
    };
  });

  return (
    <Animated.View style={animatedStyle}>
      <TouchableOpacity onPress={handlePress}>
        <Text>Press Me</Text>
      </TouchableOpacity>
    </Animated.View>
  );
}

4. Custom Worklet Functions

Extract reusable animation logic into custom worklets.

// Custom easing function
function customEasing(t) {
  'worklet';
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}

// Custom interpolation with easing
function interpolateWithEasing(value, inputRange, outputRange) {
  'worklet';

  const progress = (value - inputRange[0]) / (inputRange[1] - inputRange[0]);
  const easedProgress = customEasing(progress);

  return (
    outputRange[0] + (outputRange[1] - outputRange[0]) * easedProgress
  );
}

// Usage
const animatedStyle = useAnimatedStyle(() => {
  const opacity = interpolateWithEasing(
    scrollY.value,
    [0, 200],
    [1, 0]
  );

  return { opacity };
});

5. Derived Values

Create values that automatically update based on other shared values.

import { useDerivedValue } from 'react-native-reanimated';

function SpeedIndicator() {
  const position = useSharedValue(0);
  const lastPosition = useSharedValue(0);
  const lastTimestamp = useSharedValue(Date.now());

  // Automatically calculated speed
  const speed = useDerivedValue(() => {
    'worklet';

    const now = Date.now();
    const timeDelta = now - lastTimestamp.value;
    const positionDelta = position.value - lastPosition.value;

    lastPosition.value = position.value;
    lastTimestamp.value = now;

    return positionDelta / timeDelta; // pixels per ms
  });

  const speedStyle = useAnimatedStyle(() => {
    const color = speed.value > 5 ? 'red' : 'green';

    return {
      backgroundColor: color,
    };
  });

  return <Animated.View style={speedStyle} />;
}

Real-World Examples

Example 1: Tinder-Style Swipe Cards

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  interpolate,
  runOnJS,
} from 'react-native-reanimated';

function SwipeCards({ cards, onSwipe }) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  const gesture = Gesture.Pan()
    .onUpdate((event) => {
      'worklet';
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd((event) => {
      'worklet';

      const shouldSwipe = Math.abs(event.velocityX) > 500 ||
                          Math.abs(translateX.value) > 150;

      if (shouldSwipe) {
        const direction = translateX.value > 0 ? 'right' : 'left';

        translateX.value = withSpring(
          direction === 'right' ? 500 : -500,
          { velocity: event.velocityX },
          () => {
            runOnJS(setCurrentIndex)(currentIndex + 1);
            runOnJS(onSwipe)(direction);
            translateX.value = 0;
            translateY.value = 0;
          }
        );
      } else {
        translateX.value = withSpring(0);
        translateY.value = withSpring(0);
      }
    });

  const cardStyle = useAnimatedStyle(() => {
    const rotation = interpolate(
      translateX.value,
      [-500, 0, 500],
      [-30, 0, 30]
    );

    const opacity = interpolate(
      Math.abs(translateX.value),
      [0, 150],
      [1, 0.5]
    );

    return {
      transform: [
        { translateX: translateX.value },
        { translateY: translateY.value },
        { rotate: `${rotation}deg` },
      ],
      opacity,
    };
  });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.card, cardStyle]}>
        {cards[currentIndex]}
      </Animated.View>
    </GestureDetector>
  );
}

Example 2: Animated Bottom Sheet

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  interpolate,
  Extrapolate,
} from 'react-native-reanimated';

const SHEET_HEIGHT = 600;
const SNAP_POINTS = [SHEET_HEIGHT, 300, 50];

function BottomSheet({ children }) {
  const translateY = useSharedValue(SHEET_HEIGHT);
  const startY = useSharedValue(0);

  const findNearestSnapPoint = (value) => {
    'worklet';
    return SNAP_POINTS.reduce((prev, curr) =>
      Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
    );
  };

  const gesture = Gesture.Pan()
    .onBegin(() => {
      'worklet';
      startY.value = translateY.value;
    })
    .onUpdate((event) => {
      'worklet';
      translateY.value = Math.max(
        50,
        Math.min(SHEET_HEIGHT, startY.value + event.translationY)
      );
    })
    .onEnd((event) => {
      'worklet';

      const destination =
        event.velocityY > 500
          ? SHEET_HEIGHT
          : event.velocityY < -500
          ? 50
          : findNearestSnapPoint(translateY.value);

      translateY.value = withSpring(destination, {
        velocity: event.velocityY,
        damping: 20,
      });
    });

  const sheetStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateY: translateY.value }],
    };
  });

  const backdropStyle = useAnimatedStyle(() => {
    const opacity = interpolate(
      translateY.value,
      [50, SHEET_HEIGHT],
      [0.5, 0],
      Extrapolate.CLAMP
    );

    return { opacity };
  });

  return (
    <>
      <Animated.View style={[styles.backdrop, backdropStyle]} />
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.sheet, sheetStyle]}>
          {children}
        </Animated.View>
      </GestureDetector>
    </>
  );
}

Performance Best Practices

1. Minimize Worklet Scope

// ❌ Bad: Large worklet with many dependencies
const animatedStyle = useAnimatedStyle(() => {
  // Complex calculations
  // Many dependencies
  // Large scope
  return {
    /* ... */
  };
});

// ✅ Good: Extract to separate worklet
const calculateTransform = (value) => {
  'worklet';
  // Complex calculations isolated
  return {
    /* ... */
  };
};

const animatedStyle = useAnimatedStyle(() => {
  return calculateTransform(offset.value);
});

2. Use runOnJS Sparingly

// ❌ Bad: Frequent JS thread calls
const gesture = Gesture.Pan().onUpdate((event) => {
  'worklet';
  runOnJS(updateJSState)(event.translationX); // Called every frame!
});

// ✅ Good: Batch updates or use onEnd
const gesture = Gesture.Pan()
  .onUpdate((event) => {
    'worklet';
    offset.value = event.translationX;
  })
  .onEnd(() => {
    'worklet';
    runOnJS(updateJSState)(offset.value); // Called once
  });

3. Optimize Shared Value Reads

// ❌ Bad: Multiple reads of same value
const style = useAnimatedStyle(() => {
  const x = offset.value;
  const y = offset.value; // Unnecessary second read
  return { transform: [{ translateX: x }, { translateY: y }] };
});

// ✅ Good: Read once, use multiple times
const style = useAnimatedStyle(() => {
  const value = offset.value;
  return {
    transform: [{ translateX: value }, { translateY: value }],
  };
});

Debugging Worklets

Console Logging

// ❌ This won't work in worklets
const myWorklet = (value) => {
  'worklet';
  console.log(value); // Won't appear in console
};

// ✅ Use console from Reanimated
import { runOnJS } from 'react-native-reanimated';

const myWorklet = (value) => {
  'worklet';
  runOnJS(console.log)(value); // This works!
};

Performance Monitoring

import { measure } from 'react-native-reanimated';

const animatedStyle = useAnimatedStyle(() => {
  const start = Date.now();

  // Your animation logic
  const result = {
    /* ... */
  };

  const duration = Date.now() - start;
  if (duration > 16) {
    // More than one frame
    runOnJS(console.warn)(`Slow worklet: ${duration}ms`);
  }

  return result;
});

Common Pitfalls

1. Closure Captures

// ❌ Problem: Stale closure
function MyComponent({ initialValue }) {
  const offset = useSharedValue(0);

  const handlePress = () => {
    offset.value = withSpring(initialValue); // Captures old value!
  };

  // Fix: Use shared value
  const targetValue = useSharedValue(initialValue);

  useEffect(() => {
    targetValue.value = initialValue;
  }, [initialValue]);

  const handlePressFixed = () => {
    offset.value = withSpring(targetValue.value);
  };
}

2. Async Operations

// ❌ Worklets can't use async/await
const myWorklet = async (value) => {
  'worklet';
  const result = await fetchData(); // Won't work!
};

// ✅ Use callbacks instead
const myWorklet = (value, callback) => {
  'worklet';
  runOnJS(fetchData)(value, callback);
};

Conclusion

Worklets are a powerful tool for creating buttery-smooth, responsive UIs in React Native. By understanding how to:

  1. Leverage the UI thread for instant responses
  2. Structure complex gesture handlers efficiently
  3. Create reusable worklet functions for better code organization
  4. Optimize performance through best practices
  5. Debug effectively using proper techniques

You can build mobile experiences that rival native applications in smoothness and responsiveness.

Additional Resources


Need help implementing smooth animations in your React Native app? Contact our team for expert guidance.

Need Help with Your Project?

Our team of experts can help you implement the strategies discussed in this article. Get in touch for a free consultation.

Contact Us

Related Articles

More articles coming soon...