From fb8a6a5bb08892d9d74c607ec99f4afc9c967b04 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Wed, 5 Feb 2025 16:24:37 -0800 Subject: [PATCH] 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 --- .../ReactNativeFeatureFlags.config.js | 11 +++++ .../animated/createAnimatedPropsMemoHook.js | 49 ++++++++++++++++++- .../featureflags/ReactNativeFeatureFlags.js | 8 ++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 1714dd2ba8a..7092f033893 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -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: { diff --git a/packages/react-native/src/private/animated/createAnimatedPropsMemoHook.js b/packages/react-native/src/private/animated/createAnimatedPropsMemoHook.js index e12cb40ab1c..9d97a395e4f 100644 --- a/packages/react-native/src/private/animated/createAnimatedPropsMemoHook.js +++ b/packages/react-native/src/private/animated/createAnimatedPropsMemoHook.js @@ -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>(); + 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; - }; + } } /** diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index b91794c016d..547d82c7a05 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -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<> + * @generated SignedSource<<189673854f1226119417140b4afd46e4>> * @flow strict */ @@ -30,6 +30,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ jsOnlyTestFlag: Getter, animatedShouldDebounceQueueFlush: Getter, animatedShouldUseSingleOp: Getter, + avoidStateUpdateInAnimatedPropsMemo: Getter, disableInteractionManager: Getter, enableAccessToHostTreeInFabric: Getter, enableAnimatedClearImmediateFix: Getter, @@ -107,6 +108,11 @@ export const animatedShouldDebounceQueueFlush: Getter = createJavaScrip */ export const animatedShouldUseSingleOp: Getter = createJavaScriptFlagGetter('animatedShouldUseSingleOp', false); +/** + * Changes `useAnimatedPropsMemo` to avoid state updates to invalidate the cached `AnimatedProps`. + */ +export const avoidStateUpdateInAnimatedPropsMemo: Getter = createJavaScriptFlagGetter('avoidStateUpdateInAnimatedPropsMemo', false); + /** * Disables InteractionManager and replaces its scheduler with `setImmediate`. */