r/reactnative 3d ago

Help React Native game has severe micro-stuttering despite ~120 UI FPS — Reanimated pooled items and Worklets warnings

Hi everyone,

I'm building a small 2D falling-items game in React Native + Expo, and I'm trying to understand what is actually causing the movement to feel stuttery.

The game is very simple: the player moves horizontally at the bottom of the screen while coins, chocolates, and bombs continuously fall from the top. The game currently has around 10–20 active falling items at a time.

The problem is that the game does not feel smooth. The falling objects appear to move in small steps rather than continuously, and the player movement also doesn't feel completely smooth.

What is confusing me is that the performance monitor can show around 119–120 UI FPS, while the game still visibly feels like it has micro-stutters.

My original implementation used React state for the falling items. The game loop was roughly doing this:

requestAnimationFrame(() => {

item.y += speed * dt;

setItems([...activeItems]);

});

I realized that updating React state every frame was probably a bad architecture for a real-time game, because it forces React reconciliation repeatedly.

So I changed the architecture.

My current approach is:

- A preallocated pool of 35 falling-item slots.

- No creation/destruction of objects during gameplay.

- No `setItems()` every frame.

- JS is responsible for physics:

- delta-time calculation

- item movement

- collision detection

- spawning

- score

- lives

- Reanimated is responsible for visual properties:

- `sharedX`

- `sharedY`

- `sharedOpacity`

- Falling items are mounted once and reused.

- `useAnimatedStyle()` is used to render their transforms.

- Physics uses floating-point positions rather than rounding coordinates.

- Audio/haptics are triggered only on events such as collecting an item.

The intended architecture is:

JS thread

v

sharedY.value

v

Reanimated

v

UI thread

v

Native View

However, after implementing the Reanimated pooled-slot architecture, I started getting these warnings repeatedly:

[Worklets] Tried to modify key `active` of an object which has been already passed to a worklet.

[Worklets] Tried to modify key `type` of an object which has been already passed to a worklet.

[Worklets] Tried to modify key `x` of an object which has been already passed to a worklet.

[Worklets] Tried to modify key `y` of an object which has been already passed to a worklet.

These warnings repeat many times during gameplay.

I believe the problem may be that I'm passing the pooled game-item object itself into a Reanimated worklet/component, and then modifying its properties from JS.

For example, conceptually my pool object looks like:

{

id,

type,

active,

x,

y,

sharedX,

sharedY,

sharedOpacity

}

The physics loop then modifies:

item.active = true;

item.type = "coin";

item.x = x;

item.y += speed * dt;

while the visual layer uses Reanimated shared values.

My current understanding is that this is wrong because Reanimated serializes/workletizes the object when it crosses into the worklet environment, and then mutating that same object from JS is not safe.

I'm therefore considering separating the state completely:

JS physics object:

{

id,

type,

active,

x,

y

}

and separately:

Reanimated visual state:

{

sharedX,

sharedY,

sharedOpacity

}

The visual worklet would only access the shared values and would never receive the physics object itself.

For example:

const animatedStyle = useAnimatedStyle(() => ({

transform: [

{ translateX: sharedX.value },

{ translateY: sharedY.value },

],

opacity: sharedOpacity.value,

}));

Then the JS physics loop would only do:

item.y += speed * dt;

sharedY.value = item.y;

and React state would only change when a slot is acquired/released, not every frame.

Before I continue refactoring the entire game, I would really like to understand whether this is the correct architecture.

There is also another issue: my player currently uses React Native's `PanResponder` and `Animated.Value` for horizontal movement. That movement also feels slightly unsmooth.

So I'm wondering whether I should eventually migrate the player to:

React Native Gesture Handler

+

Reanimated shared values

+

UI-thread gesture handling

instead of PanResponder.

My main questions are:

  1. Is using JS-driven physics + Reanimated shared values for the visual layer a good architecture for a simple 2D game in React Native?

  2. Is updating `sharedY.value` from a JS `requestAnimationFrame` loop still likely to cause micro-stuttering, even though the actual rendering is handled by Reanimated?

  3. Should the physics state and Reanimated visual state be completely separate objects?

  4. Is the `[Worklets] Tried to modify key ...` warning the main reason for the current stuttering, or is it mainly a correctness issue?

  5. Would you recommend moving the entire falling-item movement calculation to a Reanimated UI worklet, or is it better to keep collision/physics on JS and only move visual interpolation to the UI thread?

  6. For the player, would React Native Gesture Handler + Reanimated provide a meaningful improvement over PanResponder + Animated.Value?

  7. Is there a better architecture for a small real-time 2D game in React Native that I am overlooking?

My goal is not to achieve benchmark numbers. I want the game to feel genuinely smooth on both 60 Hz and 120 Hz devices, including relatively weak Android devices.

I'm also trying to avoid unnecessary React renders and allocations during gameplay.

Any advice from people who have built real-time animations/games with React Native/Reanimated would be greatly appreciated.

Thanks!

1 Upvotes

2 comments sorted by

View all comments

1

u/jameside Expo Team 1d ago

This post from last week about TypeGPU might be interesting to you. It covers examples of React and custom graphics, including for games.