Lesson 22 of 26
Animations and the Native Driver
Animated.timing and useNativeDriver: true, why moving frame updates onto the native thread keeps them smooth even when JS is busy, and its real limits.
React Native ships a built-in animation system, and the single most
important thing to understand about it isn't the API surface — it's one
option, useNativeDriver, and exactly what it does to which thread does
the work.
The basic shape: Animated.Value and Animated.timing
The core building block is an Animated.Value — a number React Native
knows how to animate smoothly, as distinct from an ordinary piece of
useState you'd re-render on every frame yourself:
import { useRef } from "react";
import { Animated, Pressable } from "react-native";
function FadeInBox() {
const opacity = useRef(new Animated.Value(0)).current;
const fadeIn = () => {
Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
};
return (
<Pressable onPress={fadeIn}>
<Animated.View style={{ opacity }} />
</Pressable>
);
}Animated.timing (there's also Animated.spring, for physically-based
motion instead of a fixed duration curve) describes how opacity should
change over time, and .start() actually kicks the animation off. Note
Animated.View rather than a plain View — animating a style value
requires the animated variant of the component so React Native knows which
node to actually update as the value changes.
The one option that matters most: useNativeDriver
useNativeDriver: true is a small option with an outsized effect on how
smooth an animation actually feels. Recall from the bridge/JSI lesson that
your JS code runs on one thread, and the actual native views are rendered
on a separate native UI thread. Without the native driver, every single
frame of an animation is computed and applied through the normal JS-to-
native communication path — meaning if the JS thread is busy doing
something else (handling a large state update, running an expensive
computation, responding to a network callback), the animation genuinely has
to wait its turn, and that shows up as visible stutter.
With useNativeDriver: true, the animation's entire timeline is handed off
to the native side once, up front — and from that point, the native UI
thread runs the animation itself, frame by frame, entirely independent of
whatever the JS thread is doing. A busy JS thread can no longer make this
specific animation stutter, because it's no longer JS's job to drive it
frame by frame at all.
// Without the native driver: every frame round-trips through JS
Animated.timing(translateX, {
toValue: 100,
duration: 300,
useNativeDriver: false, // frame updates depend on the JS thread being free
}).start();
// With the native driver: handed to native, runs independently of JS load
Animated.timing(translateX, {
toValue: 100,
duration: 300,
useNativeDriver: true,
}).start();The real constraint: not every property qualifies
useNativeDriver: true isn't universally available for every style
property, and this is a genuine, long-standing limitation rather than a bug
to work around. Properties like transform (translate, scale, rotate) and
opacity are generally well-supported by the native driver. Layout-
affecting properties — things like width, height, or flexBasis — have
historically not been drivable natively, because animating layout requires
re-running layout calculations that the native driver's fast path doesn't
cover the same way. Practically, this means an animation that changes
width directly often can't use useNativeDriver: true at all, and
reaching for a transform: scale instead, where the actual visual effect
allows it, is a common, deliberate workaround.
// Often fine with the native driver — a transform, not a layout change
Animated.timing(scale, { toValue: 1.2, useNativeDriver: true, duration: 200 }).start();
// Historically not eligible for the native driver — width is a layout property
Animated.timing(widthValue, { toValue: 300, useNativeDriver: false, duration: 200 }).start();If you try to set useNativeDriver: true on a property it doesn't support,
React Native raises a clear error rather than silently doing the wrong
thing — so this constraint tends to surface quickly during development
rather than as a subtle production bug.
Beyond Animated: react-native-reanimated
For animations that need to react to gestures in real time — a card that
follows your finger and then springs back, a swipe-to-dismiss interaction —
the built-in Animated API can get unwieldy. react-native-reanimated is
the modern, more powerful library the ecosystem has largely standardized on
for that class of problem, worth knowing by name even without going deep on
its API here: it's built specifically to let complex, gesture-driven
animation logic run with the same "independent of a busy JS thread"
guarantee that useNativeDriver provides for simpler cases.
What to remember
Animated.ValueplusAnimated.timing/Animated.springand.start()is the built-in animation model;Animated.View(not a plainView) is required to animate a style value.useNativeDriver: truehands the animation to the native UI thread, so it stays smooth even if the JS thread is busy — without it, every frame round-trips through JS and can stutter under JS-thread load.- Not every property can use the native driver: transform and opacity are generally well-supported, while layout properties like width and height historically are not.
- Attempting
useNativeDriver: trueon an unsupported property raises a clear error rather than silently misbehaving. react-native-reanimatedis the ecosystem-standard, more powerful option for complex, gesture-driven animation, worth knowing by name even at a purely conceptual level.
Check yourself
4 questions · pass 3/4 to unlock The New Architecture: Fabric, TurboModules, and JSI
1.What does setting useNativeDriver: true actually change about how an Animated.timing animation runs?
2.Without useNativeDriver, what happens to an Animated value's frame updates?
3.What's a genuine, real constraint on which style properties useNativeDriver: true can animate?
4.What is react-native-reanimated, at a conceptual level?
4 left to answer