From 81b5bc26b6b5da88af0197e9be1b4150f4e709c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tar=C4=B1k?= Date: Mon, 15 Jun 2026 00:10:01 +0200 Subject: [PATCH] Optimize nested style array flattening --- .../__tests__/flattenStyle-benchmark-itest.js | 72 +++++++++++++++++++ .../StyleSheet/__tests__/flattenStyle-test.js | 31 ++++++++ .../Libraries/StyleSheet/flattenStyle.js | 41 +++++++---- 3 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-benchmark-itest.js diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-benchmark-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-benchmark-itest.js new file mode 100644 index 000000000000..02838801b57b --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-benchmark-itest.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import flattenStyle from '../flattenStyle'; +import * as Fantom from '@react-native/fantom'; + +const baseStyle = { + width: 100, + height: 40, + opacity: 0.8, + backgroundColor: 'blue', + borderRadius: 8, +}; + +const overrideStyle = { + height: 44, + opacity: 1, + borderWidth: 1, + borderColor: 'red', +}; + +const accentStyle = { + borderRadius: 12, + transform: [{scale: 1.1}], +}; + +const objectStyle = baseStyle; +const singleArrayStyle = [baseStyle]; +const singleEffectiveArrayStyle: $FlowFixMe = [ + null, + false, + undefined, + baseStyle, +]; +const nestedSingleArrayStyle: $FlowFixMe = [ + null, + [undefined, baseStyle], + false, +]; +const nestedMergedArrayStyle = [baseStyle, [null, overrideStyle, accentStyle]]; +const mergedArrayStyle = [baseStyle, overrideStyle]; + +Fantom.unstable_benchmark + .suite('flattenStyle', {minTestExecutionTimeMs: 500}) + .test('flatten object style', () => { + flattenStyle(objectStyle); + }) + .test('flatten array with one style', () => { + flattenStyle(singleArrayStyle); + }) + .test('flatten array with one effective style', () => { + flattenStyle(singleEffectiveArrayStyle); + }) + .test('flatten nested array with one effective style', () => { + flattenStyle(nestedSingleArrayStyle); + }) + .test('flatten nested array with merged styles', () => { + // $FlowFixMe[incompatible-call] + flattenStyle(nestedMergedArrayStyle); + }) + .test('flatten array with merged styles', () => { + flattenStyle(mergedArrayStyle); + }); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js index 3d42d302dab0..e380443585b4 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js @@ -94,6 +94,37 @@ describe('flattenStyle', () => { }); }); + it('should allocate an object when an array contains one style', () => { + const style = {a: 'b'}; + const singleStyle = flattenStyle([style]); + + expect(singleStyle).not.toBe(style); + expect(singleStyle).toEqual(style); + }); + + it('should allocate an object when an array contains one effective style', () => { + const style = {a: 'b'}; + const singleStyle = flattenStyle([null, false, undefined, style]); + const singleStyleAgain = flattenStyle([null, [undefined, style], false]); + + expect(singleStyle).not.toBe(style); + expect(singleStyleAgain).not.toBe(style); + expect(singleStyle).toEqual(style); + expect(singleStyleAgain).toEqual(style); + }); + + it('should allocate an object when merging multiple styles', () => { + const style1 = {width: 10}; + const style2 = {height: 20}; + const flatStyle = flattenStyle([style1, style2]); + + expect(flatStyle).not.toBe(style1); + expect(flatStyle).not.toBe(style2); + expect(style1).toEqual({width: 10}); + expect(style2).toEqual({height: 20}); + expect(flatStyle).toEqual({width: 10, height: 20}); + }); + it('should merge single class and style properly', () => { const fixture = getFixture(); const style = {styleA: 'overrideA', styleC: 'overrideC'}; diff --git a/packages/react-native/Libraries/StyleSheet/flattenStyle.js b/packages/react-native/Libraries/StyleSheet/flattenStyle.js index 5d787128aa41..0b217c126b18 100644 --- a/packages/react-native/Libraries/StyleSheet/flattenStyle.js +++ b/packages/react-native/Libraries/StyleSheet/flattenStyle.js @@ -20,6 +20,31 @@ type NonAnimatedNodeObject = TStyleProp extends AnimatedNode ? empty : TStyleProp; +function flattenStyleArrayInto< + TStyleProp extends ____DangerouslyImpreciseAnimatedStyleProp_Internal, +>(result: {[string]: $FlowFixMe}, styles: ReadonlyArray) { + for (let i = 0, styleLength = styles.length; i < styleLength; ++i) { + const style = styles[i]; + if (style === null || typeof style !== 'object') { + continue; + } + + if (Array.isArray(style)) { + // $FlowFixMe[underconstrained-implicit-instantiation] + flattenStyleArrayInto(result, style); + continue; + } + + // $FlowFixMe[invalid-in-rhs] + for (const key in style) { + // $FlowFixMe[incompatible-use] + // $FlowFixMe[invalid-computed-prop] + // $FlowFixMe[prop-missing] + result[key] = style[key]; + } + } +} + function flattenStyle< TStyleProp extends ____DangerouslyImpreciseAnimatedStyleProp_Internal, >( @@ -36,19 +61,9 @@ function flattenStyle< } const result: {[string]: $FlowFixMe} = {}; - for (let i = 0, styleLength = style.length; i < styleLength; ++i) { - // $FlowFixMe[underconstrained-implicit-instantiation] - const computedStyle = flattenStyle(style[i]); - if (computedStyle) { - // $FlowFixMe[invalid-in-rhs] - for (const key in computedStyle) { - // $FlowFixMe[incompatible-use] - // $FlowFixMe[invalid-computed-prop] - // $FlowFixMe[prop-missing] - result[key] = computedStyle[key]; - } - } - } + // $FlowFixMe[underconstrained-implicit-instantiation] + flattenStyleArrayInto(result, style); + // $FlowFixMe[incompatible-type] return result; }