From de927e61380be87f548b8933c166f8808be04eca Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Wed, 2 Oct 2024 02:07:00 -0700 Subject: [PATCH] Animated: Enqueue Completion Callbacks in Microtask (#46714) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46714 Sets up a feature flag to experiment with scheduling `Animation` completion callbacks in a microtask instead of executing them synchronously. (The completion callback is the callback passed into `animation.start()`). There are currently 4 potential scheduling behaviors of the animation completion callback: - Synchronously when `start()` is invoked, for spring and timing animations that immediately complete (e.g. no delays and not native driver). - Synchronously when `stop()` is invoked, if the animation has not yet completed. - Synchronously during the layout effect phase, or commit phase (if the `useInsertionEffectsForAnimations` feature flag is enabled), if the animation has not yet completed. - Asynchronously when the animation completes (e.g. non-zero delay or when the native driver is employed). The wide number of variables and the timing-dependent nature of these variables means that the behavior of the completion callback is very non-deterministic. Imperically, we have also found that most codebases using `useNativeDriver` mostly have completion callbacks executed asynchronously. However, this leads to surprising problems where the callback causes unpredictable behavior when it is called synchronously (e.g. when the animating component is unmounted *during* the animation). Changelog: [Internal] Reviewed By: javache Differential Revision: D63573322 fbshipit-source-id: 16c48be78c9ac65a64021249fad10f7265737c44 --- .../Libraries/Animated/animations/Animation.js | 6 +++++- .../featureflags/ReactNativeFeatureFlags.config.js | 9 +++++++++ .../src/private/featureflags/ReactNativeFeatureFlags.js | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Animated/animations/Animation.js b/packages/react-native/Libraries/Animated/animations/Animation.js index ba7601c988b..28ba043b8c7 100644 --- a/packages/react-native/Libraries/Animated/animations/Animation.js +++ b/packages/react-native/Libraries/Animated/animations/Animation.js @@ -165,7 +165,11 @@ export default class Animation { const callback = this.#onEnd; if (callback != null) { this.#onEnd = null; - callback(result); + if (ReactNativeFeatureFlags.scheduleAnimatedEndCallbackInMicrotask()) { + queueMicrotask(() => callback(result)); + } else { + callback(result); + } } } } diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index e88b40fefce..6554c5b2862 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -541,6 +541,15 @@ const definitions: FeatureFlagDefinitions = { purpose: 'release', }, }, + scheduleAnimatedEndCallbackInMicrotask: { + defaultValue: false, + metadata: { + dateAdded: '2024-09-27', + description: + 'Changes the completion callback supplied via `Animation#start` to be scheduled in a microtask instead of synchronously executed.', + purpose: 'experimentation', + }, + }, shouldSkipStateUpdatesForLoopingAnimations: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 68cd66a4174..eefbd023fe3 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<<36900baefd540f0fbb5920e9001b969c>> * @flow strict */ @@ -36,6 +36,7 @@ export type ReactNativeFeatureFlagsJsOnly = { enableAnimatedPropsMemo: Getter, enableOptimisedVirtualizedCells: Getter, isLayoutAnimationEnabled: Getter, + scheduleAnimatedEndCallbackInMicrotask: Getter, shouldSkipStateUpdatesForLoopingAnimations: Getter, shouldUseAnimatedObjectForTransform: Getter, shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter, @@ -146,6 +147,11 @@ export const enableOptimisedVirtualizedCells: Getter = createJavaScript */ export const isLayoutAnimationEnabled: Getter = createJavaScriptFlagGetter('isLayoutAnimationEnabled', true); +/** + * Changes the completion callback supplied via `Animation#start` to be scheduled in a microtask instead of synchronously executed. + */ +export const scheduleAnimatedEndCallbackInMicrotask: Getter = createJavaScriptFlagGetter('scheduleAnimatedEndCallbackInMicrotask', false); + /** * If the animation is within Animated.loop, we do not send state updates to React. */