Skip to content

Instantly share code, notes, and snippets.

@mobinni
Last active February 23, 2025 18:32
Show Gist options
  • Save mobinni/407b58b88d0da0b416df4ce290ac58c5 to your computer and use it in GitHub Desktop.
Save mobinni/407b58b88d0da0b416df4ce290ac58c5 to your computer and use it in GitHub Desktop.
React Native FPS monitor with reanimated
import React, { useEffect } from 'react';
import { TextInput } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedProps,
useAnimatedStyle,
useFrameCallback,
useSharedValue,
} from 'react-native-reanimated';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
export const FPSMonitor = () => {
const fps = useSharedValue(0);
const lastTimestamp = useSharedValue(0);
const framesCount = useSharedValue(0);
const totalCount = useSharedValue(0);
const jsFps = useSharedValue(0);
const positionX = useSharedValue(0);
const positionY = useSharedValue(0);
const animatedRootStyles = useAnimatedStyle(() => {
return {
transform: [
{ translateX: positionX.value },
{ translateY: positionY.value },
],
};
});
const _onPanHandlerStateChange = useAnimatedGestureHandler({
onStart: (_, ctx: Record<string, number>) => {
ctx.startX = positionX.value;
ctx.startY = positionY.value;
},
onActive: (event, ctx: Record<string, number>) => {
positionX.value = ctx.startX + event.translationX;
positionY.value = ctx.startY + event.translationY;
},
});
// UI thread
useFrameCallback((frameInfo) => {
if (lastTimestamp.value === 0) {
lastTimestamp.value = frameInfo.timestamp;
return;
}
framesCount.value += 1;
const elapsed = frameInfo.timestamp - lastTimestamp.value;
if (elapsed >= 1000) {
totalCount.value += 1;
const newFPS = Math.min((framesCount.value * 1000) / elapsed, 60);
fps.value = Math.round(newFPS);
lastTimestamp.value = frameInfo.timestamp;
framesCount.value = 0;
}
});
// Measure JS Thread FPS
useEffect(() => {
let frameCount = 0;
let start = Date.now();
let rafId: number;
const updateFps = () => {
'worklet';
frameCount += 1;
const now = Date.now();
const elapsed = now - start;
if (elapsed >= 1000) {
jsFps.value = Math.min(Math.round((frameCount * 1000) / elapsed), 60);
start = now;
frameCount = 0;
}
rafId = requestAnimationFrame(updateFps);
};
rafId = requestAnimationFrame(updateFps);
return () => cancelAnimationFrame(rafId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const animatedFPS = useAnimatedProps(() => {
return {
text: `${fps.value}`,
defaultValue: `${fps.value}`,
};
});
const animatedJsFPS = useAnimatedProps(() => {
return {
text: `${jsFps.value}`,
defaultValue: `${jsFps.value}`,
};
});
return (
<PanGestureHandler onHandlerStateChange={_onPanHandlerStateChange}>
<Animated.View
style={[
animatedRootStyles,
{
position: 'absolute',
top: 20,
right: 20,
width: 75,
backgroundColor: 'black',
padding: 8,
borderRadius: 4,
},
]}
>
<Animated.View style={{ flexDirection: 'row', flex: 1 }}>
<Animated.Text
style={{ color: 'white', fontSize: 14, paddingRight: 8 }}
>
UI:
</Animated.Text>
<AnimatedTextInput
style={{ color: 'white', fontSize: 14, width: '100%' }}
animatedProps={animatedFPS}
/>
</Animated.View>
<Animated.View style={{ flexDirection: 'row', flex: 1 }}>
<Animated.Text
style={{ color: 'white', fontSize: 14, paddingRight: 8 }}
>
JS:
</Animated.Text>
<AnimatedTextInput
style={{ color: 'white', fontSize: 14, width: '100%' }}
animatedProps={animatedJsFPS}
/>
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment