mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
bdb394f754
commit
fb8a6a5bb0
@@ -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`.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user