Skip to content

Commit d26f7b3

Browse files
tarikfpmeta-codesync[bot]
authored andcommitted
Optimize flattenStyle for nested style arrays (#57203)
Summary: - Walk nested style arrays directly into one result object instead of recursively allocating intermediate flattened objects. - Preserve existing behavior for object styles, falsy entries, and later-style override order. - Add focused unit coverage for array inputs that must still allocate merged result objects. ## Benchmark proof External reproducible benchmark app: https://github.com/tarikfp/rn-style-flatten-benchmark Latest `yarn bench:compare` from the benchmark repo compares React Native `main` at `066c0d8bd8` against this branch at `81b5bc26b6`: | scenario | before median ms | after median ms | change | | --- | ---: | ---: | ---: | | nested single style array | 294.79 | 98.73 | 66.5% faster | | nested merged style array | 278.54 | 122.06 | 56.2% faster | This is not a blanket claim that every React Native screen becomes 50%+ faster. It is targeted to the `flattenStyle` path when composed components pass nested style arrays. ## Validation - `yarn test` in benchmark app repo - `yarn lint` in benchmark app repo - `yarn workspace style-flatten-benchmark-app tsc --noEmit` - `yarn bench:compare` - `yarn test packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js --runInBand` in the React Native checkout - iOS simulator build/install for the benchmark app Draft while broader upstream validation is gathered. ## Affected areas `flattenStyle` is not only the public `StyleSheet.flatten` helper. Core React Native components call it when they need to read or normalize style props before passing work further down: - `Text` uses it in `packages/react-native/Libraries/Text/Text.js:188` before normalizing text style values such as numeric `fontWeight` and text selection-related props. - `Image` uses it on both platforms: `Image.ios.js:141` reads `objectFit`, `resizeMode`, and `tintColor`; `Image.android.js:310` reads `objectFit` and `resizeMode` before building native props. - `ImageBackground` uses it in `packages/react-native/Libraries/Image/ImageBackground.js:72` before splitting size-related style values between the wrapper view and inner image. - `TextInput` uses it in `packages/react-native/Libraries/Components/TextInput/TextInput.js:655` while preserving the original style when possible, but still flattening to normalize text style overrides. - `TouchableOpacity` uses it in `packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js:260` and `:364` to read opacity from `style` when setting and updating its animated opacity. - `ScrollView` uses it in `packages/react-native/Libraries/Components/ScrollView/ScrollView.js:1663` for development-time layout warnings and in `:1850` when splitting outer and inner layout props around refresh controls. - `Animated.ScrollView` uses the same split path in `packages/react-native/Libraries/Animated/components/AnimatedScrollView.js:94`. - Animated props use it in `packages/react-native/Libraries/Animated/nodes/AnimatedProps.js:62` and `:157`, and the newer memo hook uses it in `packages/react-native/src/private/animated/createAnimatedPropsMemoHook.js:125`, so nested style arrays also matter for animated style props. - Fabric public instances use it in `packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload.js:186` and `:195` before diffing array style props. - Developer tooling also goes through it: the element inspector uses it in `ElementProperties.js:43` and `ElementBox.js:34`, and React DevTools receives it as the React Native style resolver from `setUpReactDevTools.js:74`. That is why the benchmark focuses on nested style arrays instead of claiming every render gets faster. The change helps when those call sites receive styles composed like `style={[base, condition && extra, [override]]}`. ## Changelog: [GENERAL] [CHANGED] - Make `flattenStyle` avoid extra intermediate objects when flattening nested style arrays. Pull Request resolved: #57203 Test Plan: - Ran the focused React Native unit test: `yarn test packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js --runInBand`. - Ran the external benchmark app checks: `yarn test`, `yarn lint`, and `yarn workspace style-flatten-benchmark-app tsc --noEmit`. - Ran `yarn bench:compare` in the benchmark app repo. The latest saved result compares React Native `main` at `066c0d8bd8` with this branch at `81b5bc26b6` and shows the nested style-array cases improving from `294.79 ms` to `98.73 ms` and from `278.54 ms` to `122.06 ms`. - Built and installed the benchmark app on iOS simulators to compare a `main` build and this branch side by side. Reviewed By: javache Differential Revision: D108616011 Pulled By: Abbondanzo fbshipit-source-id: f1540a274b6919c43d137d6587ea4d11be258796
1 parent 9d54391 commit d26f7b3

3 files changed

Lines changed: 131 additions & 13 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import flattenStyle from '../flattenStyle';
14+
import * as Fantom from '@react-native/fantom';
15+
16+
const baseStyle = {
17+
width: 100,
18+
height: 40,
19+
opacity: 0.8,
20+
backgroundColor: 'blue',
21+
borderRadius: 8,
22+
};
23+
24+
const overrideStyle = {
25+
height: 44,
26+
opacity: 1,
27+
borderWidth: 1,
28+
borderColor: 'red',
29+
};
30+
31+
const accentStyle = {
32+
borderRadius: 12,
33+
transform: [{scale: 1.1}],
34+
};
35+
36+
const objectStyle = baseStyle;
37+
const singleArrayStyle = [baseStyle];
38+
const singleEffectiveArrayStyle: $FlowFixMe = [
39+
null,
40+
false,
41+
undefined,
42+
baseStyle,
43+
];
44+
const nestedSingleArrayStyle: $FlowFixMe = [
45+
null,
46+
[undefined, baseStyle],
47+
false,
48+
];
49+
const nestedMergedArrayStyle = [baseStyle, [null, overrideStyle, accentStyle]];
50+
const mergedArrayStyle = [baseStyle, overrideStyle];
51+
52+
Fantom.unstable_benchmark
53+
.suite('flattenStyle', {minTestExecutionTimeMs: 500})
54+
.test('flatten object style', () => {
55+
flattenStyle(objectStyle);
56+
})
57+
.test('flatten array with one style', () => {
58+
flattenStyle(singleArrayStyle);
59+
})
60+
.test('flatten array with one effective style', () => {
61+
flattenStyle(singleEffectiveArrayStyle);
62+
})
63+
.test('flatten nested array with one effective style', () => {
64+
flattenStyle(nestedSingleArrayStyle);
65+
})
66+
.test('flatten nested array with merged styles', () => {
67+
// $FlowFixMe[incompatible-call]
68+
flattenStyle(nestedMergedArrayStyle);
69+
})
70+
.test('flatten array with merged styles', () => {
71+
flattenStyle(mergedArrayStyle);
72+
});

packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,37 @@ describe('flattenStyle', () => {
9494
});
9595
});
9696

97+
it('should allocate an object when an array contains one style', () => {
98+
const style = {a: 'b'};
99+
const singleStyle = flattenStyle([style]);
100+
101+
expect(singleStyle).not.toBe(style);
102+
expect(singleStyle).toEqual(style);
103+
});
104+
105+
it('should allocate an object when an array contains one effective style', () => {
106+
const style = {a: 'b'};
107+
const singleStyle = flattenStyle([null, false, undefined, style]);
108+
const singleStyleAgain = flattenStyle([null, [undefined, style], false]);
109+
110+
expect(singleStyle).not.toBe(style);
111+
expect(singleStyleAgain).not.toBe(style);
112+
expect(singleStyle).toEqual(style);
113+
expect(singleStyleAgain).toEqual(style);
114+
});
115+
116+
it('should allocate an object when merging multiple styles', () => {
117+
const style1 = {width: 10};
118+
const style2 = {height: 20};
119+
const flatStyle = flattenStyle([style1, style2]);
120+
121+
expect(flatStyle).not.toBe(style1);
122+
expect(flatStyle).not.toBe(style2);
123+
expect(style1).toEqual({width: 10});
124+
expect(style2).toEqual({height: 20});
125+
expect(flatStyle).toEqual({width: 10, height: 20});
126+
});
127+
97128
it('should merge single class and style properly', () => {
98129
const fixture = getFixture();
99130
const style = {styleA: 'overrideA', styleC: 'overrideC'};

packages/react-native/Libraries/StyleSheet/flattenStyle.js

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ type NonAnimatedNodeObject<TStyleProp> = TStyleProp extends AnimatedNode
2020
? empty
2121
: TStyleProp;
2222

23+
function flattenStyleArrayInto<
24+
TStyleProp extends ____DangerouslyImpreciseAnimatedStyleProp_Internal,
25+
>(result: {[string]: $FlowFixMe}, styles: ReadonlyArray<?TStyleProp>) {
26+
for (let i = 0, styleLength = styles.length; i < styleLength; ++i) {
27+
const style = styles[i];
28+
if (style === null || typeof style !== 'object') {
29+
continue;
30+
}
31+
32+
if (Array.isArray(style)) {
33+
// $FlowFixMe[underconstrained-implicit-instantiation]
34+
flattenStyleArrayInto(result, style);
35+
continue;
36+
}
37+
38+
// $FlowFixMe[invalid-in-rhs]
39+
for (const key in style) {
40+
// $FlowFixMe[incompatible-use]
41+
// $FlowFixMe[invalid-computed-prop]
42+
// $FlowFixMe[prop-missing]
43+
result[key] = style[key];
44+
}
45+
}
46+
}
47+
2348
function flattenStyle<
2449
TStyleProp extends ____DangerouslyImpreciseAnimatedStyleProp_Internal,
2550
>(
@@ -36,19 +61,9 @@ function flattenStyle<
3661
}
3762

3863
const result: {[string]: $FlowFixMe} = {};
39-
for (let i = 0, styleLength = style.length; i < styleLength; ++i) {
40-
// $FlowFixMe[underconstrained-implicit-instantiation]
41-
const computedStyle = flattenStyle(style[i]);
42-
if (computedStyle) {
43-
// $FlowFixMe[invalid-in-rhs]
44-
for (const key in computedStyle) {
45-
// $FlowFixMe[incompatible-use]
46-
// $FlowFixMe[invalid-computed-prop]
47-
// $FlowFixMe[prop-missing]
48-
result[key] = computedStyle[key];
49-
}
50-
}
51-
}
64+
// $FlowFixMe[underconstrained-implicit-instantiation]
65+
flattenStyleArrayInto(result, style);
66+
5267
// $FlowFixMe[incompatible-type]
5368
return result;
5469
}

0 commit comments

Comments
 (0)