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:
- Leverage the UI thread for instant responses
- Structure complex gesture handlers efficiently
- Create reusable worklet functions for better code organization
- Optimize performance through best practices
- 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.
