From 50bd67ddc7c5375faf6d96e94b62be2cfdb7bcf0 Mon Sep 17 00:00:00 2001 From: Mad Dinh Date: Sat, 22 Aug 2026 15:57:09 +0700 Subject: [PATCH] Animate filter without forcing a Fabric commit every frame The C++ direct-manipulation allowlist is missing 'filter', which NativeAnimatedAllowlist.js lists as an animatable style. Anything absent from that set is classified as a layout update by StyleAnimatedNode, so filter animations are committed through the shadow tree each frame instead of taking the direct-manipulation path. --- .../internal/NativeAnimatedAllowlist.h | 1 + .../animated/tests/AnimatedNodeTests.cpp | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/animated/internal/NativeAnimatedAllowlist.h b/packages/react-native/ReactCommon/react/renderer/animated/internal/NativeAnimatedAllowlist.h index deb94193261c..3fc0649bd41a 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/internal/NativeAnimatedAllowlist.h +++ b/packages/react-native/ReactCommon/react/renderer/animated/internal/NativeAnimatedAllowlist.h @@ -48,6 +48,7 @@ inline const std::unordered_set &getDirectManipulationAllowlist() "borderStartStartRadius", "elevation", "opacity", + "filter", "transform", "zIndex", /* ios styles */ diff --git a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp index fd5f0545105c..075f7a598e03 100644 --- a/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp @@ -7,6 +7,7 @@ #include "AnimationTestsBase.h" +#include #include #include #include @@ -332,4 +333,61 @@ TEST_F(AnimatedNodeTests, ObjectAnimatedNode) { EXPECT_EQ(collectedProps["test"][2]["scale3d"], 4); } +// Styles that Animated supports and that do not participate in layout must be +// direct-manipulation eligible; anything absent from this set is treated as a +// layout update by StyleAnimatedNode and forced through a Fabric commit. +// Keep in sync with SUPPORTED_STYLES in +// packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js. +TEST_F(AnimatedNodeTests, directManipulationAllowlistCoversNonLayoutStyles) { + const auto& allowlist = getDirectManipulationAllowlist(); + + for (const auto& style : + {"backgroundColor", + "borderBottomColor", + "borderColor", + "borderEndColor", + "borderLeftColor", + "borderRightColor", + "borderStartColor", + "borderTopColor", + "color", + "tintColor", + "borderBottomEndRadius", + "borderBottomLeftRadius", + "borderBottomRightRadius", + "borderBottomStartRadius", + "borderEndEndRadius", + "borderEndStartRadius", + "borderRadius", + "borderTopEndRadius", + "borderTopLeftRadius", + "borderTopRightRadius", + "borderTopStartRadius", + "borderStartEndRadius", + "borderStartStartRadius", + "elevation", + "opacity", + "filter", + "transform", + "zIndex", + "shadowOpacity", + "shadowRadius", + "scaleX", + "scaleY", + "translateX", + "translateY"}) { + EXPECT_EQ(allowlist.count(style), 1u) + << style + << " is animatable and does not affect layout, so it must be " + "direct-manipulation eligible"; + } + + // Layout styles must stay out, so they keep going through Fabric. + for (const auto& style : + {"width", "height", "margin", "padding", "flex", "top", "gap"}) { + EXPECT_EQ(allowlist.count(style), 0u) + << style << " affects layout and must not be direct-manipulated"; + } +} + } // namespace facebook::react