|
2 | 2 |
|
3 | 3 | Low overhead monitoring of important performance metrics for React Native apps like JS FPS, UI FPS, CPU usage, and memory usage. |
4 | 4 |
|
| 5 | +## Installation |
| 6 | + |
| 7 | +```bash |
| 8 | +bun add react-native-performance-toolkit react-native-nitro-modules |
| 9 | +``` |
| 10 | + |
5 | 11 | ## Requirements |
6 | 12 |
|
7 | 13 | - React Native v0.76.0 or higher |
8 | 14 | - Node 18.0.0 or higher |
| 15 | +- Reanimated v4 or higher |
9 | 16 |
|
10 | | -> [!IMPORTANT] |
11 | | -> To Support `Nitro Views` you need to install React Native version v0.78.0 or higher. |
| 17 | +## Usage |
12 | 18 |
|
13 | | -## Installation |
| 19 | +#### Very simple usage - get value once |
14 | 20 |
|
15 | | -```bash |
16 | | -bun add react-native-performance-toolkit react-native-nitro-modules |
| 21 | +```tsx |
| 22 | +import { |
| 23 | + getJsFps, |
| 24 | + getUiFps, |
| 25 | + getCpuUsage, |
| 26 | + getMemoryUsage, |
| 27 | + getDeviceMaxRefreshRate, |
| 28 | + getDeviceCurrentRefreshRate, |
| 29 | +} from 'react-native-performance-toolkit' |
| 30 | + |
| 31 | +const jsFps = getJsFps() |
| 32 | +const uiFps = getUiFps() |
| 33 | +const cpuUsage = getCpuUsage() |
| 34 | +const memoryUsage = getMemoryUsage() |
| 35 | +const maxRefreshRate = getDeviceMaxRefreshRate() |
| 36 | +const currentRefreshRate = getDeviceCurrentRefreshRate() |
| 37 | + |
| 38 | +console.log('JS FPS:', jsFps) |
| 39 | +console.log('UI FPS:', uiFps) |
| 40 | +console.log('CPU Usage:', cpuUsage) |
| 41 | +console.log('Memory Usage:', memoryUsage) |
| 42 | +console.log('Max Refresh Rate:', maxRefreshRate, 'Hz') |
| 43 | +console.log('Current Refresh Rate:', currentRefreshRate, 'Hz') |
| 44 | +``` |
| 45 | + |
| 46 | +#### Subscribe to changes |
| 47 | + |
| 48 | +```tsx |
| 49 | +import { onFpsJsChange } from 'react-native-performance-toolkit' |
| 50 | + |
| 51 | +const unsubscribe = onFpsJsChange((fps) => { |
| 52 | + console.log('JS FPS changed:', fps) |
| 53 | +}) |
| 54 | + |
| 55 | +// later |
| 56 | +unsubscribe() |
| 57 | +``` |
| 58 | + |
| 59 | +#### React Hooks - JS Thread only |
| 60 | + |
| 61 | +Please be aware that this hook will not update if your JS thread is blocked (0 FPS) because updates are happening only that very same thread. |
| 62 | + |
| 63 | +```tsx |
| 64 | +import { useFpsJs } from 'react-native-performance-toolkit' |
| 65 | + |
| 66 | +const SomeComponent = () => { |
| 67 | + const jsFps = useFpsJs() |
| 68 | + return <Text>JS FPS: {jsFps}</Text> |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +#### Reanimated Hooks - UI Thread |
| 73 | + |
| 74 | +To avoid issue with not showing 0 FPS it's recommended to use Reanimated based hooks or pre-made components. That's will ensure that value is updated even if the JS thread is blocked. |
| 75 | + |
| 76 | +```tsx |
| 77 | +import { TextInput } from 'react-native' |
| 78 | +import { useFpsJsSharedValue } from 'react-native-performance-toolkit' |
| 79 | +import Animated, { useAnimatedReaction } from 'react-native-reanimated' |
| 80 | + |
| 81 | +const AnimatedTextInput = Animated.createAnimatedComponent(TextInput) |
| 82 | + |
| 83 | +const SomeComponent = () => { |
| 84 | + const inputRef = useAnimatedRef<TextInput>() |
| 85 | + const uiFps = useFpsJsSharedValue() |
| 86 | + |
| 87 | + useAnimatedReaction( |
| 88 | + () => uiFps.value.toString(), |
| 89 | + (value) => { |
| 90 | + setNativeProps(inputRef, { text: value }) |
| 91 | + } |
| 92 | + ) |
| 93 | + |
| 94 | + return <AnimatedTextInput ref={inputRef} /> |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +#### Pre-made Reanimated components - UI Thread |
| 99 | + |
| 100 | +For better DX library provides pre-made Reanimated components that runs solely on UI thread. |
| 101 | + |
| 102 | +```tsx |
| 103 | +import { UIThreadReanimatedCounter } from 'react-native-performance-toolkit' |
| 104 | + |
| 105 | +const SomeComponent = () => { |
| 106 | + return ( |
| 107 | + <> |
| 108 | + <UIThreadReanimatedCounter label="JS FPS" type="js" /> |
| 109 | + <UIThreadReanimatedCounter label="UI FPS" type="ui" /> |
| 110 | + <UIThreadReanimatedCounter label="CPU" type="cpu" /> |
| 111 | + <UIThreadReanimatedCounter label="RAM" type="memory" /> |
| 112 | + </> |
| 113 | + ) |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +#### Direct buffer access (advanced usage, experimental) |
| 118 | + |
| 119 | +Some advanced usage might require direct buffer access. For example you might want to use this library in custom native component or you might want to use it in worklet thread. This is experimental and might be changed in the future. |
| 120 | + |
| 121 | +```tsx |
| 122 | +import { |
| 123 | + getJsFpsBuffer, |
| 124 | + getUiFpsBuffer, |
| 125 | + getCpuUsageBuffer, |
| 126 | + getMemoryUsageBuffer, |
| 127 | +} from 'react-native-performance-toolkit' |
| 128 | + |
| 129 | +const jsFpsBuffer = getJsFpsBuffer() |
| 130 | +const uiFpsBuffer = getUiFpsBuffer() |
| 131 | +const cpuUsageBuffer = getCpuUsageBuffer() |
| 132 | +const memoryUsageBuffer = getMemoryUsageBuffer() |
| 133 | + |
| 134 | +const getValueFromBuffer = (buffer: ArrayBuffer) => { |
| 135 | + const view = new DataView(buffer) |
| 136 | + return view.getInt32(0, true) // true = littleEndian |
| 137 | +} |
| 138 | + |
| 139 | +console.log('JS FPS:', getValueFromBuffer(jsFpsBuffer)) |
| 140 | +console.log('UI FPS:', getValueFromBuffer(uiFpsBuffer)) |
| 141 | +console.log('CPU Usage:', getValueFromBuffer(cpuUsageBuffer)) |
| 142 | +console.log('Memory Usage:', getValueFromBuffer(memoryUsageBuffer)) |
| 143 | +``` |
| 144 | + |
| 145 | +### Access from worklets (advanced usage) |
| 146 | + |
| 147 | +You can also access value from any worklet thread, but to do that you need use (Nitro Modules unboxing function)[https://nitro.margelo.com/docs/worklets]. For more detailed implementation look for [source code of UI Reanimated hooks like `useFpsJsSharedValue`](https://github.com/Nodonisko/react-native-performance-toolkit/blob/main/src/hooks/uiThreadHooks.ts). |
| 148 | + |
| 149 | +```tsx |
| 150 | +import { useCallback } from 'react' |
| 151 | +import { |
| 152 | + BoxedJsFpsTracking, |
| 153 | + BoxedPerformanceToolkit, |
| 154 | +} from 'react-native-performance-toolkit' |
| 155 | + |
| 156 | +// ... |
| 157 | + |
| 158 | +const updateFps = useCallback(() => { |
| 159 | + 'worklet' |
| 160 | + const unboxedJsFps = BoxedJsFpsTracking.unbox() |
| 161 | + const unboxedPerformanceToolkit = BoxedPerformanceToolkit.unbox() |
| 162 | + |
| 163 | + let buffer = unboxedJsFps.getJsFpsBuffer() |
| 164 | + |
| 165 | + const view = new DataView(buffer) |
| 166 | + const value = view.getInt32(0, true) |
| 167 | + |
| 168 | + console.log('JS FPS:', value) |
| 169 | + |
| 170 | + // update shared value for example |
| 171 | + fpsValue.value = value |
| 172 | +}, []) |
17 | 173 | ``` |
18 | 174 |
|
19 | | -## Credits |
| 175 | +## API Reference |
| 176 | + |
| 177 | +- **Simple getters** |
| 178 | + - `getJsFps(): number` - Returns current JS FPS (0-60) |
| 179 | + - `getUiFps(): number` - Returns current UI FPS (0-30/60/90/120/...) |
| 180 | + - `getCpuUsage(): number` - Returns CPU usage percentage in Linux format |
| 181 | + - `getMemoryUsage(): number` - Returns memory usage in bytes |
| 182 | + - `getDeviceMaxRefreshRate(): number` - Returns device's maximum supported refresh rate (e.g., 120 Hz on ProMotion devices) |
| 183 | + - `getDeviceCurrentRefreshRate(): number` - Returns device's current active refresh rate (may be lower than max on adaptive refresh rate displays) |
| 184 | + |
| 185 | +- **Subscription functions** |
| 186 | + - `onFpsJsChange(callback: (fps: number) => void): () => void` - Subscribe to JS FPS changes |
| 187 | + - `onFpsUiChange(callback: (fps: number) => void): () => void` - Subscribe to UI FPS changes |
| 188 | + - `onCpuChange(callback: (value: number) => void): () => void` - Subscribe to CPU usage changes |
| 189 | + - `onMemoryChange(callback: (value: number) => void): () => void` - Subscribe to memory usage changes |
| 190 | + |
| 191 | +- **React Hooks (JS Thread)** |
| 192 | + - `useFpsJs(): number` - Hook that returns current JS FPS |
| 193 | + - `useFpsUi(): number` - Hook that returns current UI FPS |
| 194 | + - `useCpuUsage(): number` - Hook that returns current CPU usage |
| 195 | + - `useMemoryUsage(): number` - Hook that returns current memory usage |
| 196 | + |
| 197 | +- **React Components (runs on UI Thread)** |
| 198 | + - `<JSFpsCounter />` - Pre-made component displaying JS FPS |
| 199 | + - `<UIFpsCounter />` - Pre-made component displaying UI FPS |
| 200 | + - `<CpuUsageCounter />` - Pre-made component displaying CPU usage |
| 201 | + - `<MemoryUsageCounter />` - Pre-made component displaying memory usage |
| 202 | + |
| 203 | +- **Buffer-based API** |
| 204 | + - `getJsFpsBuffer(): ArrayBuffer` - Returns ArrayBuffer with JS FPS data |
| 205 | + - `getUiFpsBuffer(): ArrayBuffer` - Returns ArrayBuffer with UI FPS data |
| 206 | + - `getCpuUsageBuffer(): ArrayBuffer` - Returns ArrayBuffer with CPU usage data |
| 207 | + - `getMemoryUsageBuffer(): ArrayBuffer` - Returns ArrayBuffer with memory usage data |
| 208 | + |
| 209 | +- **Advanced (Nitro Modules)** |
| 210 | + - `BoxedJsFpsTracking` - Direct boxed Nitro module instance for worklet usage |
| 211 | + - `getJsFpsBuffer(): ArrayBuffer` |
| 212 | + - `BoxedPerformanceToolkit` - Direct boxed Nitro module instance for worklet usage |
| 213 | + - `getUiFpsBuffer(): ArrayBuffer` |
| 214 | + - `getCpuUsageBuffer(): ArrayBuffer` |
| 215 | + - `getMemoryUsageBuffer(): ArrayBuffer` |
| 216 | + - `getDeviceMaxRefreshRate(): number` |
| 217 | + - `getDeviceCurrentRefreshRate(): number` |
| 218 | + |
| 219 | +## Architecture |
| 220 | + |
| 221 | +#### Low overhead tracking |
| 222 | + |
| 223 | +On Android library is reading values from virtual files like `/proc/stat` for CPU usage and `/proc/smaps_rollup` for memory usage. This is very low overhead and doesn't require any additional permissions. |
| 224 | + |
| 225 | +On iOS library is reading values from `task_vm_info`/`rusage` direct kernel call. This is also super very low overhead. |
| 226 | + |
| 227 | +#### Device Refresh Rate |
| 228 | + |
| 229 | +The library provides two methods for getting refresh rate information: |
| 230 | + |
| 231 | +- **Max Refresh Rate**: Returns the maximum supported refresh rate of the device |
| 232 | + - Android: Uses `Display.supportedModes` to find the highest available refresh rate |
| 233 | + - iOS: Uses `UIScreen.main.maximumFramesPerSecond` |
20 | 234 |
|
21 | | -Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module). |
| 235 | +- **Current Refresh Rate**: Returns the currently active refresh rate |
| 236 | + - Android: Uses `Display.getRefreshRate()` - useful for devices with adaptive refresh rate (e.g., 60/90/120Hz switching) |
| 237 | + - iOS: Uses `CADisplayLink.preferredFramesPerSecond` - useful for ProMotion displays that dynamically adjust |
22 | 238 |
|
23 | 239 | ## Contributing |
24 | 240 |
|
|
0 commit comments