Number Flow React Native
Guides

Performance Tips

How to get the most out of animated number transitions

You shouldn't need to think about performance in most cases — the library handles the heavy lifting internally. This page covers what's already optimized for you and a few situations where you can help.

What's already handled

Format object caching

You don't need to worry about passing inline format objects. The library serializes format options via JSON.stringify internally, so structurally identical objects won't trigger extra work even if their reference changes on every render:

// This is fine — the library deduplicates format objects internally
function Price({ value }: { value: number }) {
  return (
    <NumberFlow
      value={value}
      format={{ style: 'currency', currency: 'USD' }}
      style={style}
    />
  );
}

Intl.NumberFormat instances are also cached globally, so multiple components with the same format share a single formatter.

That said, defining format objects as constants is still good practice for readability:

const currencyFormat = { style: 'currency', currency: 'USD' } as const;

Progressive mount (Native renderer)

Native components (NumberFlow, TimeFlow) render a plain Text element on the first frame, then swap to the full animated slot tree on the next frame via requestAnimationFrame. This avoids the cost of instantiating all the animated hooks during the initial mount, so the component appears instantly without a blank flash.

Skia components don't need this — canvas rendering doesn't have the same view hierarchy overhead.

Memoized child slots

Individual digit and symbol slots are wrapped in React.memo, so when the parent re-renders but a slot's props haven't changed, it bails out early. Layout computations, glyph metrics, and format keys are all memoized internally too.

Reduce Motion

When the device's "Reduce Motion" setting is on (and respectMotionPreference is true, which is the default), all animation durations collapse to zero. Transitions become instant — values snap to their final position without interpolation across multiple frames.

When you should optimize

Frequent parent re-renders

NumberFlow doesn't use React.memo at the top level, so it re-renders whenever its parent does. The internal memoization keeps this cheap when the value hasn't changed, but if your parent is re-rendering very frequently (e.g. on every gesture frame), wrapping the number in memo avoids the overhead entirely:

const MemoizedPrice = React.memo(function MemoizedPrice({ value }: { value: number }) {
  return (
    <NumberFlow
      value={value}
      format={currencyFormat}
      style={style}
    />
  );
});

Many simultaneous instances

If you're rendering a lot of animated numbers at once (dashboards, tables, charts), prefer SkiaNumberFlow inside a single Canvas. A single canvas with multiple Skia components is lighter than the equivalent number of native View trees since everything draws on one surface.

High-frequency value updates

When the value changes at high frequency — gesture-driven scrubbing, sensor data, timers faster than ~100ms — use SkiaNumberFlow with the sharedValue prop. This keeps digit updates on the UI thread entirely, bypassing React re-renders:

const formatted = useDerivedValue(() => `${speed.value.toFixed(0)}`);

<SkiaNumberFlow sharedValue={formatted} font={font} color="#000" />

Note that sharedValue expects a SharedValue<string> (a pre-formatted string), not a number. You're responsible for formatting in the worklet.

Without sharedValue, each value change goes through a React re-render on the JS thread, which adds latency and can drop frames during fast gestures.

In scrubbing mode, the library pre-allocates a pool of 20 SharedValue slots at mount time. Digit extraction happens entirely on the UI thread via useAnimatedReaction, writing directly to these pre-allocated values without crossing the JS bridge.

Timing config objects

Unlike format objects, transformTiming, spinTiming, and opacityTiming are plain objects compared by reference. If you're defining custom timings, keep them outside the component to avoid unnecessary re-renders of the internal slot components:

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

const timing = { duration: 600, easing: Easing.out(Easing.cubic) };

function Price({ value }: { value: number }) {
  return <NumberFlow value={value} transformTiming={timing} style={style} />;
}

On this page