Day 38: UI Threading & Animations: Reanimated 3 Engine Mechanics
Understand worklets and the UI thread execution model well enough to write animations that never drop a frame due to JS thread congestion.
Study
Concepts
Why animations need to escape the JS thread
Even with JSI removing serialization cost (Day 32), driving an animation frame-by-frame FROM the JS thread still means every frame's update depends on the JS thread being free at that exact moment — if the JS thread is busy (a network response handler, a big re-render), the animation can visibly stutter, because JS is single-threaded (Day 1) and animation logic competes with everything else running there.
Reanimated 3 solves this by compiling small JS functions marked with the `'worklet'` directive into a form that can run directly on the UI thread (via JSI, not the old Bridge), completely independent of whatever the JS thread is doing. Gesture-driven and layout animations can therefore stay smooth even while the JS thread is busy with unrelated work.
Shared Values: state that lives on both threads
A `useSharedValue(initial)` creates a value accessible from BOTH the JS and UI threads without going through React's render cycle at all — mutating `.value` inside a worklet updates the UI thread's copy of that animated property directly, so a drag gesture can update a view's position at native frame rate with zero React re-renders in the loop. `useAnimatedStyle` reads shared values inside a worklet and produces a style object that Reanimated applies directly on the UI thread each frame.
This is the core mental shift from CSS/Animated-API thinking: you are not calling `setState` 60 times a second — you are mutating a shared value that the UI thread already knows how to consume, entirely outside React's reconciliation loop.
See It
Visualizations
Visualization
Worklets running on the UI thread, independent of JS thread load
Build It
Code Examples
A drag gesture driven entirely on the UI thread
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
// Runs as a worklet, ON THE UI THREAD — no JS thread round trip per frame
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0); // snap back, still on the UI thread
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.card, animatedStyle]} />
</GestureDetector>
);
}Reading a shared value back on the JS thread when needed
function ScrollProgress({ scrollY }) {
const [progressLabel, setProgressLabel] = useState('0%');
// runOnJS explicitly hops FROM the UI thread BACK to the JS thread —
// use sparingly; every hop re-introduces the cost this pattern avoids.
useAnimatedReaction(
() => scrollY.value,
(current) => {
runOnJS(setProgressLabel)(`${Math.round(current)}%`);
}
);
return <Text>{progressLabel}</Text>;
}Remember
Key Takeaways
- Driving animations from the JS thread makes them vulnerable to JS thread congestion — worklets avoid this entirely.
- 'worklet' functions run directly on the UI thread via JSI, independent of what the JS thread is doing at that moment.
- useSharedValue creates state readable/writable from both threads, updated OUTSIDE React's render/reconciliation cycle.
- useAnimatedStyle reads shared values inside a worklet to produce a style applied every frame on the UI thread.
- runOnJS is the deliberate, occasional escape hatch back to the JS thread — every use re-adds a thread hop, so use it sparingly.
Do It
Practice
- 1Build the DraggableCard example and, while dragging, trigger a deliberately slow synchronous JS operation — confirm the drag stays smooth.
- 2Convert a React state + setInterval-driven animation to a Reanimated shared value + withTiming/withSpring version and compare frame smoothness.
- 3Use useAnimatedReaction + runOnJS to sync a scroll position into a normal React state label, and explain why this hop is necessary here.