Skip to content
Danny's Blog
Twitter

A simple FPS counter for React Native

FPS, react native, react, hook1 min read

If you've ever wanted to optimize performance on a react native app you've probably used the built in frame rate monitor from the dev menu. You've also probably read in the documentation that performance in dev mode is much worse. This means it's hard to get a real picture of what your frame rate is for production apps.

To get around this I have been using a custom hook and component to get an idea of FPS in a local release build. From my experience it's fairly close to the same as the JS fps that you get from the dev tools.

The code here is largely based on an example found online by my colleague. I've just adjusted it for my needs. You can see the original here

Arguably the fps counter itself could have some impact on performance, however for some simple comparisons it's useful since if there is an impact it will be the same in both cases. If you see any area for improvement please let me know!

1import { useEffect, useState } from "react";
2
3type FrameData = {
4 fps: number;
5 lastStamp: number;
6 framesCount: number;
7 average: number;
8 totalCount: number;
9};
10export type FPS = { average: FrameData["average"]; fps: FrameData["fps"] };
11
12export function useFPSMetric(): FPS {
13 const [frameState, setFrameState] = useState<FrameData>({
14 fps: 0,
15 lastStamp: Date.now(),
16 framesCount: 0,
17 average: 0,
18 totalCount: 0,
19 });
20
21 useEffect(() => {
22 // NOTE: timeout is here
23 // because requestAnimationFrame is deferred
24 // and to prevent setStates when unmounted
25 let timeout: NodeJS.Timeout | null = null;
26
27 requestAnimationFrame((): void => {
28 timeout = setTimeout((): void => {
29 const currentStamp = Date.now();
30 const shouldSetState = currentStamp - frameState.lastStamp > 1000;
31
32 const newFramesCount = frameState.framesCount + 1;
33 // updates fps at most once per second
34 if (shouldSetState) {
35 const newValue = frameState.framesCount;
36 const totalCount = frameState.totalCount + 1;
37 // I use math.min here because values over 60 aren't really important
38 // I calculate the mean fps incrementatally here instead of storing all the values
39 const newMean = Math.min(
40 frameState.average + (newValue - frameState.average) / totalCount,
41 60
42 );
43 setFrameState({
44 fps: frameState.framesCount,
45 lastStamp: currentStamp,
46 framesCount: 0,
47 average: newMean,
48 totalCount,
49 });
50 } else {
51 setFrameState({
52 ...frameState,
53 framesCount: newFramesCount,
54 });
55 }
56 }, 0);
57 });
58 return () => {
59 if (timeout) clearTimeout(timeout);
60 };
61 }, [frameState]);
62
63 return { average: frameState.average, fps: frameState.fps };
64}

I then put this in a simple component at the root of the project and make it toggle-able.

Here is an example of that

1import React, { FunctionComponent } from "react";
2import { StyleSheet, Text, View } from "react-native";
3import { useFPSMetric } from "./useFPSMetrics";
4
5const styles = StyleSheet.create({
6 text: { color: "white" },
7 container: { position: "absolute", top: 100, left: 8 },
8});
9
10export const FpsCounter: FunctionComponent<{ visible: boolean }> = ({
11 visible,
12}) => {
13 const { fps, average } = useFPSMetric();
14 if (!visible) return null;
15 return (
16 <View pointerEvents={"none"} style={styles.container}>
17 <Text style={styles.text}>{fps} FPS</Text>
18 <Text style={styles.text}>{average.toFixed(2)} average FPS</Text>
19 </View>
20 );
21};

Then in the app entry point

1export default (): ReactElement => (
2 <View>
3 <App />
4 <FpsCounter visible={true} />
5 </View>
6);

It's important to have it at the root of the app otherwise some updates to the FPS might be delayed and the FPS won't be accurate.

Hopefully that's useful to someone out there and if you have a better way to measure JS FPS in release config then please share that so we can all improve together.

Thanks for taking the time to read my post, heres my github if you want to see my other work.