Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3190,10 +3190,10 @@ SPEC CHECKSUMS:
RNReanimated: 3b47c33660454c6f9700b463e92daa282030866a
RNScreens: 6ced6ae8a526512a6eef6e28c2286e1fc2d378c3
RNSVG: 287504b73fa0e90a605225aa9f852a86d5461e84
RNWorklets: 7119ae08263033c456c80d90794a312f2f88c956
RNWorklets: 991f94e4fa31fc20853e74d5d987426f8580cb0d
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changed when I installed pods. Not sure if it'd better to add these changes in a separate PR instead.

SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: e80c5fabbc3e26311152fa20404cdfa14f16a11f

PODFILE CHECKSUM: 5d8c04f461eed0f22e86610877d94f2b8b838b8b
PODFILE CHECKSUM: db099f48c6dadedd8fc0a430129b75e561867ab9

COCOAPODS: 1.15.2
3 changes: 2 additions & 1 deletion packages/react-native-reanimated/android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ string(
-DREANIMATED_FEATURE_FLAGS=\"${REANIMATED_FEATURE_FLAGS}\"")
string(APPEND CMAKE_CXX_FLAGS " -fno-omit-frame-pointer -fstack-protector-all")
# flags to optimize the binary size
string(APPEND CMAKE_CXX_FLAGS " -fvisibility=hidden -ffunction-sections -fdata-sections")
string(APPEND CMAKE_CXX_FLAGS
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well, I ran the yarn format command and this has changed.

" -fvisibility=hidden -ffunction-sections -fdata-sections")

if(${REANIMATED_PROFILING})
string(APPEND CMAKE_CXX_FLAGS " -DREANIMATED_PROFILING")
Expand Down
67 changes: 67 additions & 0 deletions packages/react-native-reanimated/src/ViewDescriptorsMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';
import type { SharedValue, StyleUpdaterContainer } from './commonTypes';
import { makeMutable } from './core';
import type { Descriptor, ViewTag } from './hook/commonTypes';

export type ShareableViewDescriptors = SharedValue<
ReadonlyMap<ViewTag, Descriptor>
> & {
toArray: () => Descriptor[];
};

export interface ViewDescriptorsMap {
shareableViewDescriptors: ShareableViewDescriptors;

/**
* Adds a new view descriptor to the set
*
* @param item - The descriptor to add
* @param updaterContainer - Optional container with style updater function
*/
add: (item: Descriptor, updaterContainer?: StyleUpdaterContainer) => void;

/**
* Removes a view descriptor from the set by its tag
*
* @param viewTag - The tag of the view descriptor to remove
*/
remove: (viewTag: ViewTag) => void;
}

export function makeViewDescriptorsMap(): ViewDescriptorsMap {
const sharedDescriptors = makeMutable<Map<ViewTag, Descriptor>>(new Map());
const cachedArray = makeMutable<Descriptor[] | null>(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the performance of this new solution with a cacheable array. Usually, the view descriptors container doesn't contain more than 3 elements (mostly just one) So, finding the index with linear complexity isn't too bad in this context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we have a FlatList with hundreds of elements and each of them share the same animated style? It would register/unregister the style quite often and doing it in O(n) for each of elements doesn't seem to be performant.

Copy link
Member

@piaskowyk piaskowyk Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it could be problematic in this cases, but it's not the primary use case. I just want to ensure that this doesn't introduce a performance regression when we have many components, each with its own style (not shared).

Copy link
Member Author

@MatiPl01 MatiPl01 Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see. The map is used only during renders, so the performance difference might be noticeable only during renders, not on every animations frame, so I think it is not that bad. Animations still use the cached array, which is invalidated only when new animated styles are added or old are removed (very rare cases).

Wrapping up, it shouldn't affect the performance of animations but may affect the performance of renders if we have multiple views with separate animated styles each (still I feel that this performance loss shouldn't be that much noticeable but I haven't measured the impact).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's another case to consider. Let's imagine a long FlatList where every component uses the same animated style. The animation is playing (for example skeleton animation). As we scroll down, we'll create new animated components, and each time we have to recreate the whole array. Maybe we can replace the recreation of an array with simply pushing new descriptor to it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it usually works in such a way that new elements are rendered but old ones are removed at the same time when we scroll the list. In this case, pushing won't give us any benefit as we won't be able to perform a fast removal of elements, so the array would have to still be re-created.

The old implementation re-created it on each .remove() call made to the ViewDescriptorsSet, so for n elements removed, its time complexity was O(n^2). Now, all updates are batched until the next animation frame is executed. Thanks to this, we will perform all removals before the array is re-created so it is still better than it was.

If we consider only additions, then yes, we could optimize it a bit by pushing new descriptors to the array but I think it is a very rare case to have more elements added without old ones being removed.


return {
shareableViewDescriptors: {
...(sharedDescriptors as SharedValue<ReadonlyMap<ViewTag, Descriptor>>),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you spread sharedDescriptors mutable here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I want to add an additional field to it - the toArray method. It allowed me to get rid of type casting in the packages/react-native-reanimated/src/screenTransition/styleUpdater.ts file and to easily get the array (usually cached) from the SharedValue via viewDescriptors.toArray() in the packages/react-native-reanimated/src/updateProps/updateProps.ts.

And, since the Mutable is a JS object, extending it in this way seems to be fine.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's safe. Mutables use the reference from makeMutable so spreading it might lead to undefined behavior. It'd be better to call Object.defineProperty instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both implementations of makeMutable (i.e. makeMutableNative and makeMutableWeb) create a mutable object, which is a plain JS object so I think my approach is correct. We don't reference the mutable itself via this in the mutable object methods and every method is just a function which should still be able to use values from its closure after being destructed.

I can change it to Object.defineProperty if you prefer but it shouldn't change the behavior (at least I can't see how it may change it).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call serializableMappingCache.set(mutable, handle);. I'd rather use Object.defineProperty for safety.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Tomek, it is safer to avoid a spread operator in that case.

toArray: () => {
'worklet';
if (!cachedArray.value) {
cachedArray.value = Array.from(sharedDescriptors.value.values());
}
return cachedArray.value;
Comment on lines +40 to +43
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I should use the .modify method here. I think I don't have to as I am never modifying the already stored value (I always assign a new one), so the .value assignment seems to be fine.

},
},

add: (item: Descriptor, updaterContainer?: StyleUpdaterContainer) => {
const updater = updaterContainer?.current;
sharedDescriptors.modify((descriptors) => {
'worklet';
descriptors.set(item.tag, item);
cachedArray.value = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we can just push to an array instead of create new one?

updater?.(true);
return descriptors;
}, false);
},

remove: (viewTag: ViewTag) => {
sharedDescriptors.modify((descriptors) => {
'worklet';
descriptors.delete(viewTag);
cachedArray.value = null;
return descriptors;
}, false);
},
};
}
47 changes: 0 additions & 47 deletions packages/react-native-reanimated/src/ViewDescriptorsSet.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { StyleProps } from '../commonTypes';
import { isSharedValue } from '../isSharedValue';
import { startMapper, stopMapper } from '../mappers';
import { updateProps } from '../updateProps';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsMap } from '../ViewDescriptorsMap';
import { makeViewDescriptorsMap } from '../ViewDescriptorsMap';
import type {
AnimatedComponentProps,
AnimatedComponentTypeInternal,
Expand Down Expand Up @@ -124,7 +124,7 @@ export function getInlineStyle(
}

export class InlinePropManager implements IInlinePropManager {
_inlinePropsViewDescriptors: ViewDescriptorsSet | null = null;
_inlinePropsViewDescriptors: ViewDescriptorsMap | null = null;
_inlinePropsMapperId: number | null = null;
_inlineProps: StyleProps = {};

Expand All @@ -138,7 +138,7 @@ export class InlinePropManager implements IInlinePropManager {

if (hasChanged) {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
this._inlinePropsViewDescriptors = makeViewDescriptorsMap();

const { viewTag, shadowNodeWrapper } = viewInfo;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import type {
} from '../commonTypes';
import type { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import type { BaseAnimationBuilder } from '../layoutReanimation';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsMap } from '../ViewDescriptorsMap';

export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
viewDescriptors?: ViewDescriptorsMap;
initial?: SharedValue<StyleProps>;
styleUpdaterContainer?: StyleUpdaterContainer;
}
Expand Down
8 changes: 5 additions & 3 deletions packages/react-native-reanimated/src/hook/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import type {
AnimatedProps,
} from '../createAnimatedComponent';
import type { ReanimatedHTMLElement } from '../ReanimatedModule/js-reanimated';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsMap } from '../ViewDescriptorsMap';

export type DependencyList = Array<unknown> | undefined;

export type ViewTag = number | ReanimatedHTMLElement;

export interface Descriptor {
tag: number | ReanimatedHTMLElement;
tag: ViewTag;
shadowNodeWrapper: ShadowNodeWrapper;
}

Expand Down Expand Up @@ -111,7 +113,7 @@ export interface IWorkletEventHandler<Event extends object> {
export interface AnimatedStyleHandle<
Style extends DefaultStyle | AnimatedProps = DefaultStyle,
> {
viewDescriptors: ViewDescriptorsSet;
viewDescriptors: ViewDescriptorsMap;
initial: {
value: AnimatedStyle<Style>;
updater: () => AnimatedStyle<Style>;
Expand Down
16 changes: 9 additions & 7 deletions packages/react-native-reanimated/src/hook/useAnimatedStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import type {
import { startMapper, stopMapper } from '../core';
import type { AnimatedProps } from '../createAnimatedComponent/commonTypes';
import { updateProps, updatePropsJestWrapper } from '../updateProps';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import type {
ShareableViewDescriptors,
ViewDescriptorsMap,
} from '../ViewDescriptorsMap';
import { makeViewDescriptorsMap } from '../ViewDescriptorsMap';
import type {
AnimatedStyleHandle,
DefaultStyle,
DependencyList,
Descriptor,
JestAnimatedStyleHandle,
} from './commonTypes';
import { useSharedValue } from './useSharedValue';
Expand All @@ -50,7 +52,7 @@ interface AnimatedUpdaterData {
updater: () => AnimatedStyle<any>;
};
remoteState: AnimatedState;
viewDescriptors: ViewDescriptorsSet;
viewDescriptors: ViewDescriptorsMap;
styleUpdaterContainer: StyleUpdaterContainer;
}

Expand Down Expand Up @@ -195,7 +197,7 @@ function runAnimations(
}

function styleUpdater(
viewDescriptors: SharedValue<Descriptor[]>,
viewDescriptors: ShareableViewDescriptors,
updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
state: AnimatedState,
animationsActive: SharedValue<boolean>,
Expand Down Expand Up @@ -303,7 +305,7 @@ function styleUpdater(
}

function jestStyleUpdater(
viewDescriptors: SharedValue<Descriptor[]>,
viewDescriptors: ShareableViewDescriptors,
updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
state: AnimatedState,
animationsActive: SharedValue<boolean>,
Expand Down Expand Up @@ -520,7 +522,7 @@ For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/doc
isAnimationCancelled: false,
isAnimationRunning: false,
}),
viewDescriptors: makeViewDescriptorsSet(),
viewDescriptors: makeViewDescriptorsMap(),
styleUpdaterContainer: { current: undefined },
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ function applyStyleForTopScreen(
screenTransitionConfig;
const { topScreenStyle: computeTopScreenStyle } = screenTransition;
const topScreenStyle = computeTopScreenStyle(event, screenDimensions);
const viewDescriptorsArray = [
createViewDescriptor(topScreenId) as Descriptor,
];
const topScreenDescriptor = {
value: [createViewDescriptor(topScreenId)] as Descriptor[],
toArray: () => viewDescriptorsArray,
};
updateProps(topScreenDescriptor, topScreenStyle, undefined);
}
Expand All @@ -39,8 +42,11 @@ export function applyStyleForBelowTopScreen(
event,
screenDimensions
);
const viewDescriptorsArray = [
createViewDescriptor(belowTopScreenId) as Descriptor,
];
const belowTopScreenDescriptor = {
value: [createViewDescriptor(belowTopScreenId)] as Descriptor[],
toArray: () => viewDescriptorsArray,
};
updateProps(belowTopScreenDescriptor, belowTopScreenStyle, undefined);
}
Expand Down
30 changes: 15 additions & 15 deletions packages/react-native-reanimated/src/updateProps/updateProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,30 @@ import type { Descriptor } from '../hook/commonTypes';
import type { ReanimatedHTMLElement } from '../ReanimatedModule/js-reanimated';
import { _updatePropsJS } from '../ReanimatedModule/js-reanimated';

/**
* Usually the `ShareableViewDescriptors` type would be passed, but objects that
* have just a `toArray` method are fine too.
*/
type ViewDescriptors = {
toArray: () => Descriptor[];
};

let updateProps: (
viewDescriptors: ViewDescriptorsWrapper,
viewDescriptors: ViewDescriptors,
updates: PropUpdates,
isAnimatedProps?: boolean
) => void;

if (SHOULD_BE_USE_WEB) {
updateProps = (viewDescriptors, updates, isAnimatedProps) => {
'worklet';
viewDescriptors.value?.forEach((viewDescriptor) => {
for (const viewDescriptor of viewDescriptors.toArray()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for ... of loops are faster than forEach so I decided to change them as well as a part of the view descriptors optimization.

const component = viewDescriptor.tag as ReanimatedHTMLElement;
if ('boxShadow' in updates) {
updates.boxShadow = processBoxShadowWeb(updates.boxShadow);
}
_updatePropsJS(updates, component, isAnimatedProps);
});
}
};
} else {
updateProps = (viewDescriptors, updates) => {
Expand All @@ -62,7 +70,7 @@ if (SHOULD_BE_USE_WEB) {
}

export const updatePropsJestWrapper = (
viewDescriptors: ViewDescriptorsWrapper,
viewDescriptors: ViewDescriptors,
updates: AnimatedStyle<any>,
animatedValues: RefObject<AnimatedStyle<any>>,
adapters: ((updates: AnimatedStyle<any>) => void)[]
Expand Down Expand Up @@ -112,8 +120,8 @@ function createUpdatePropsManager() {
}, {});

return {
update(viewDescriptors: ViewDescriptorsWrapper, updates: PropUpdates) {
viewDescriptors.value.forEach(({ tag, shadowNodeWrapper }) => {
update(viewDescriptors: ViewDescriptors, updates: PropUpdates) {
for (const { tag, shadowNodeWrapper } of viewDescriptors.toArray()) {
const viewTag = tag as number;
const { nativePropUpdates, jsPropUpdates } = processViewUpdates(
viewTag,
Expand All @@ -137,7 +145,7 @@ function createUpdatePropsManager() {
queueMicrotask(this.flush);
flushPending = true;
}
});
}
},
flush(this: void) {
if (nativeOperations.length) {
Expand Down Expand Up @@ -179,11 +187,3 @@ if (SHOULD_BE_USE_WEB) {
global.UpdatePropsManager = createUpdatePropsManager();
})();
}

/**
* This used to be `SharedValue<Descriptors[]>` but objects holding just a
* single `value` prop are fine too.
*/
interface ViewDescriptorsWrapper {
value: Readonly<Descriptor[]>;
}
Loading