Animated: Avoid In-Band State Update (#49184)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49184

D65645985 shipped a refactor to `Animated`, so that it would use a custom `useAnimatedPropsMemo` instead of `useMemo`. This significantly improved update performance by no longer invalidating the `AnimatedProps` on effectively every update to `Animated` components.

However, this was measured to increase memory usage. After a few experiments, we identified that use of the in-band state update was responsible for the memory regression. While this requires further root cause investigation, this diff attempts to mitigate the memory regression.

This diff introduces a feature flag that enables an implementation that minimizes duplicated work, such as unnecessarily computing `compositeKey` or creating new instances of `AnimatedProps`. In addition, this implementation strives to do so without significantly degrading when an update is interrupted by a concurrent update.

Changelog:
[General][Changed] - Introduced a feature flag to test an optimization in `Animated` to reduce memory usage.

Reviewed By: rickhanlonii

Differential Revision: D69135223

fbshipit-source-id: a2699a314625e7570698bc41455b139711cfd7e3
This commit is contained in:
Tim Yung
2025-02-05 16:24:37 -08:00
committed by Facebook GitHub Bot
parent bdb394f754
commit fb8a6a5bb0
3 changed files with 65 additions and 3 deletions
@@ -508,6 +508,17 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
avoidStateUpdateInAnimatedPropsMemo: {
defaultValue: false,
metadata: {
dateAdded: '2025-02-05',
description:
'Changes `useAnimatedPropsMemo` to avoid state updates to invalidate the cached `AnimatedProps`.',
expectedReleaseValue: true,
purpose: 'experimentation',
},
ossReleaseStage: 'none',
},
disableInteractionManager: {
defaultValue: false,
metadata: {
@@ -17,8 +17,9 @@ import {AnimatedEvent} from '../../../Libraries/Animated/AnimatedEvent';
import AnimatedNode from '../../../Libraries/Animated/nodes/AnimatedNode';
import {isPlainObject} from '../../../Libraries/Animated/nodes/AnimatedObject';
import flattenStyle from '../../../Libraries/StyleSheet/flattenStyle';
import * as ReactNativeFeatureFlags from '../featureflags/ReactNativeFeatureFlags';
import nullthrows from 'nullthrows';
import {useMemo, useState} from 'react';
import {useInsertionEffect, useMemo, useRef, useState} from 'react';
type CompositeKey = {
style?: {[string]: CompositeKeyComponent},
@@ -64,6 +65,50 @@ export function createAnimatedPropsMemoHook(
return function useAnimatedPropsMemo(
create: () => AnimatedProps,
props: $ReadOnly<{[string]: mixed}>,
): AnimatedProps {
// NOTE: This feature flag must be evaluated inside the hook because this
// module factory can be evaluated much sooner, before overrides are set.
const useAnimatedPropsImpl =
ReactNativeFeatureFlags.avoidStateUpdateInAnimatedPropsMemo()
? useAnimatedPropsMemo_ref
: useAnimatedPropsMemo_state;
return useAnimatedPropsImpl(create, props);
};
function useAnimatedPropsMemo_ref(
create: () => AnimatedProps,
props: $ReadOnly<{[string]: mixed}>,
): AnimatedProps {
const compositeKey = useMemo(
() => createCompositeKeyForProps(props, allowlist),
[props],
);
const prevRef = useRef<?$ReadOnly<{
compositeKey: typeof compositeKey,
node: AnimatedProps,
}>>();
const prev = prevRef.current;
const next =
prev != null &&
areCompositeKeysEqual(prev.compositeKey, compositeKey, allowlist)
? prev
: {
compositeKey,
node: create(),
};
useInsertionEffect(() => {
prevRef.current = next;
}, [next]);
return next.node;
}
function useAnimatedPropsMemo_state(
create: () => AnimatedProps,
props: $ReadOnly<{[string]: mixed}>,
): AnimatedProps {
const compositeKey = useMemo(
() => createCompositeKeyForProps(props, allowlist),
@@ -91,7 +136,7 @@ export function createAnimatedPropsMemoHook(
});
}
return state.value;
};
}
}
/**
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<d807507f5d26ccb2ba6849d40ed88e7a>>
* @generated SignedSource<<189673854f1226119417140b4afd46e4>>
* @flow strict
*/
@@ -30,6 +30,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
jsOnlyTestFlag: Getter<boolean>,
animatedShouldDebounceQueueFlush: Getter<boolean>,
animatedShouldUseSingleOp: Getter<boolean>,
avoidStateUpdateInAnimatedPropsMemo: Getter<boolean>,
disableInteractionManager: Getter<boolean>,
enableAccessToHostTreeInFabric: Getter<boolean>,
enableAnimatedClearImmediateFix: Getter<boolean>,
@@ -107,6 +108,11 @@ export const animatedShouldDebounceQueueFlush: Getter<boolean> = createJavaScrip
*/
export const animatedShouldUseSingleOp: Getter<boolean> = createJavaScriptFlagGetter('animatedShouldUseSingleOp', false);
/**
* Changes `useAnimatedPropsMemo` to avoid state updates to invalidate the cached `AnimatedProps`.
*/
export const avoidStateUpdateInAnimatedPropsMemo: Getter<boolean> = createJavaScriptFlagGetter('avoidStateUpdateInAnimatedPropsMemo', false);
/**
* Disables InteractionManager and replaces its scheduler with `setImmediate`.
*/