From ba1166395c6bbb9f3a80277cc46bfcb0d02eeee3 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Sat, 28 Feb 2026 13:55:18 -0800 Subject: [PATCH 1/2] Optimize processBoxShadow with pre-compiled regex patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Hoist regex patterns to module-level constants to avoid recompiling them on every function call. This optimization targets a hotspot identified via JS sampling profiler. Benchmark results show 6-7% improvement in "views with large props and styles" tests: - render 100 views with large props/styles: 9.48ms → 8.89ms (-6.2%) - render 1500 views with large props/styles: 137.2ms → 127.5ms (-7.0%) Reviewed By: javache, NickGerleman Differential Revision: D92153667 --- .../Libraries/StyleSheet/processBoxShadow.js | 56 ++++++++++++++++++- .../ReactNativeFeatureFlags.config.js | 11 ++++ .../featureflags/ReactNativeFeatureFlags.js | 8 ++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/StyleSheet/processBoxShadow.js b/packages/react-native/Libraries/StyleSheet/processBoxShadow.js index 0d1aa710d29e..a61b45b8b3ec 100644 --- a/packages/react-native/Libraries/StyleSheet/processBoxShadow.js +++ b/packages/react-native/Libraries/StyleSheet/processBoxShadow.js @@ -11,8 +11,23 @@ import type {ProcessedColorValue} from './processColor'; import type {BoxShadowValue} from './StyleSheetTypes'; +import {enableOptimizedBoxShadowParsing} from '../../src/private/featureflags/ReactNativeFeatureFlags'; import processColor from './processColor'; +// Pre-compiled regex patterns for performance - avoids regex compilation on each call +const COMMA_SPLIT_REGEX = /,(?![^()]*\))/; +const WHITESPACE_SPLIT_REGEX = /\s+(?![^(]*\))/; +const LENGTH_PARSE_REGEX = /^([+-]?\d*\.?\d+)(px)?$/; +const NEWLINE_REGEX = /\n/g; + +let _optimizedBoxShadowParsing: ?boolean; +function isOptimizedBoxShadowParsingEnabled(): boolean { + if (_optimizedBoxShadowParsing == null) { + _optimizedBoxShadowParsing = enableOptimizedBoxShadowParsing(); + } + return _optimizedBoxShadowParsing; +} + export type ParsedBoxShadow = { offsetX: number, offsetY: number, @@ -32,7 +47,12 @@ export default function processBoxShadow( const boxShadowList = typeof rawBoxShadows === 'string' - ? parseBoxShadowString(rawBoxShadows.replace(/\n/g, ' ')) + ? parseBoxShadowString( + rawBoxShadows.replace( + isOptimizedBoxShadowParsingEnabled() ? NEWLINE_REGEX : /\n/g, + ' ', + ), + ) : rawBoxShadows; for (const rawBoxShadow of boxShadowList) { @@ -109,7 +129,11 @@ function parseBoxShadowString(rawBoxShadows: string): Array { let result: Array = []; for (const rawBoxShadow of rawBoxShadows - .split(/,(?![^()]*\))/) // split by comma that is not in parenthesis + .split( + isOptimizedBoxShadowParsingEnabled() + ? COMMA_SPLIT_REGEX + : /,(?![^()]*\))/, + ) // split by comma that is not in parenthesis .map(bS => bS.trim()) .filter(bS => bS !== '')) { const boxShadow: BoxShadowValue = { @@ -123,7 +147,11 @@ function parseBoxShadowString(rawBoxShadows: string): Array { let lengthCount = 0; // split rawBoxShadow string by all whitespaces that are not in parenthesis - const args = rawBoxShadow.split(/\s+(?![^(]*\))/); + const args = rawBoxShadow.split( + isOptimizedBoxShadowParsingEnabled() + ? WHITESPACE_SPLIT_REGEX + : /\s+(?![^(]*\))/, + ); for (const arg of args) { const processedColor = processColor(arg); if (processedColor != null) { @@ -192,6 +220,28 @@ function parseBoxShadowString(rawBoxShadows: string): Array { } function parseLength(length: string): ?number { + if (isOptimizedBoxShadowParsingEnabled()) { + // Use pre-compiled regex for performance + const match = LENGTH_PARSE_REGEX.exec(length); + + if (!match) { + return null; + } + + const value = parseFloat(match[1]); + if (Number.isNaN(value)) { + return null; + } + + // match[2] is 'px' or undefined + // If no unit and value is not 0, return null + if (match[2] == null && value !== 0) { + return null; + } + + return value; + } + // matches on args with units like "1.5 5% -80deg" const argsWithUnitsRegex = /([+-]?\d*(\.\d+)?)([\w\W]+)?/g; const match = argsWithUnitsRegex.exec(length); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index e8e429ba609c..4eb200696066 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -990,6 +990,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableOptimizedBoxShadowParsing: { + defaultValue: false, + metadata: { + dateAdded: '2026-02-26', + description: + 'Hoists regex patterns to module scope and optimizes parseLength in processBoxShadow for improved performance.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, externalElementInspectionEnabled: { defaultValue: true, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 7939dec2f939..72f8145cf4f2 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<<0d5cf6a975687f5bf540b16a997809a7>> * @flow strict * @noformat */ @@ -33,6 +33,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ animatedShouldUseSingleOp: Getter, deferFlatListFocusChangeRenderUpdate: Getter, disableMaintainVisibleContentPosition: Getter, + enableOptimizedBoxShadowParsing: Getter, externalElementInspectionEnabled: Getter, fixImageSrcDimensionPropagation: Getter, fixVirtualizeListCollapseWindowSize: Getter, @@ -158,6 +159,11 @@ export const deferFlatListFocusChangeRenderUpdate: Getter = createJavaS */ export const disableMaintainVisibleContentPosition: Getter = createJavaScriptFlagGetter('disableMaintainVisibleContentPosition', false); +/** + * Hoists regex patterns to module scope and optimizes parseLength in processBoxShadow for improved performance. + */ +export const enableOptimizedBoxShadowParsing: Getter = createJavaScriptFlagGetter('enableOptimizedBoxShadowParsing', false); + /** * Enable the external inspection API for DevTools to communicate with the Inspector overlay. */ From 3d7cacaa8bb58940e4b733630c531d2532c64b6a Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Sat, 28 Feb 2026 13:55:18 -0800 Subject: [PATCH 2/2] Migrate processBoxShadow tests to Fantom Summary: changelog: [internal] Add Fantom integration test for processBoxShadow. Uses `fantom_flags enableOptimizedBoxShadowParsing:*` to automatically test both code paths. Differential Revision: D94796393 --- .../__tests__/processBoxShadow-itest.js | 354 ++++++++++++++++++ .../__tests__/processBoxShadow-test.js | 346 ----------------- 2 files changed, 354 insertions(+), 346 deletions(-) create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-itest.js delete mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-itest.js new file mode 100644 index 000000000000..f6c408728bdf --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-itest.js @@ -0,0 +1,354 @@ +/** + * 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 + * @fantom_flags enableOptimizedBoxShadowParsing:* + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import processBoxShadow from '../processBoxShadow'; +import processColor from '../processColor'; + +describe('processBoxShadow', () => { + describe('string parsing', () => { + it('should parse basic string', () => { + expect(processBoxShadow('10px 5px')).toEqual([ + { + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse basic string with unitless zero length', () => { + expect(processBoxShadow('10px 0')).toEqual([ + { + offsetX: 10, + offsetY: 0, + }, + ]); + }); + + it('should parse basic string with multiple whitespaces', () => { + expect(processBoxShadow('10px 5px')).toEqual([ + { + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse string with color', () => { + expect(processBoxShadow('red 10px 5px')).toEqual([ + { + color: processColor('red'), + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse string with color function rgba', () => { + expect(processBoxShadow('rgba(255, 255, 255, 0.5) 10px 5px')).toEqual([ + { + color: processColor('rgba(255, 255, 255, 0.5)'), + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse string with color function hsl', () => { + expect(processBoxShadow('hsl(318, 69%, 55%) 10px 5px')).toEqual([ + { + color: processColor('hsl(318, 69%, 55%)'), + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse string with hex color', () => { + expect(processBoxShadow('#FFFFFF 10px 5px')).toEqual([ + { + color: processColor('#FFFFFF'), + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse string with blurRadius', () => { + expect(processBoxShadow('red 10px 5px 2px')).toEqual([ + { + color: processColor('red'), + blurRadius: 2, + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse string with spreadDistance', () => { + expect(processBoxShadow('red 10px 5px 2px 3px')).toEqual([ + { + color: processColor('red'), + blurRadius: 2, + offsetX: 10, + offsetY: 5, + spreadDistance: 3, + }, + ]); + }); + + it('should parse string arguments with units', () => { + expect(processBoxShadow('5px 2px')).toEqual([ + { + offsetX: 5, + offsetY: 2, + }, + ]); + }); + + it('should parse string with inset', () => { + expect(processBoxShadow('5px 2px inset')).toEqual([ + { + offsetX: 5, + offsetY: 2, + inset: true, + }, + ]); + }); + + it('should parse string with inset and color before and after lengths', () => { + expect(processBoxShadow('red 10px 10px inset')).toEqual([ + { + color: processColor('red'), + offsetX: 10, + offsetY: 10, + inset: true, + }, + ]); + }); + + it('should parse multiple box shadow strings', () => { + expect( + processBoxShadow( + '10px 5px red, 5px 12px inset, inset 10px 45px 13px red', + ), + ).toEqual([ + { + offsetX: 10, + offsetY: 5, + color: processColor('red'), + }, + { + offsetX: 5, + offsetY: 12, + inset: true, + }, + { + offsetX: 10, + offsetY: 45, + blurRadius: 13, + inset: true, + color: processColor('red'), + }, + ]); + }); + + it('should parse multiple box shadow strings with newlines', () => { + expect( + processBoxShadow( + '10px 5px red, 5px 12px inset,\n inset 10px 45px 13px red', + ), + ).toEqual([ + { + offsetX: 10, + offsetY: 5, + color: processColor('red'), + }, + { + offsetX: 5, + offsetY: 12, + inset: true, + }, + { + offsetX: 10, + offsetY: 45, + blurRadius: 13, + inset: true, + color: processColor('red'), + }, + ]); + }); + + it('should parse string with leading dot decimal', () => { + expect(processBoxShadow('.5px 2px')).toEqual([ + { + offsetX: 0.5, + offsetY: 2, + }, + ]); + }); + + it('should parse string with explicit positive sign', () => { + expect(processBoxShadow('+5px 2px')).toEqual([ + { + offsetX: 5, + offsetY: 2, + }, + ]); + }); + }); + + describe('string parsing failures', () => { + it('should fail to parse string with invalid units', () => { + expect(processBoxShadow('red 10em 5$ 2| 3rp')).toEqual([]); + }); + + it('should fail to parse too many lengths', () => { + expect(processBoxShadow('10px 5px 2px 3px 10px 10px')).toEqual([]); + }); + + it('should fail to parse inset between lengths', () => { + expect(processBoxShadow('10px inset 5px 2px 3px,')).toEqual([]); + }); + + it('should fail to parse double color', () => { + expect(processBoxShadow('red red 10px 5px')).toEqual([]); + }); + + it('should fail to parse double inset', () => { + expect(processBoxShadow('10px 5px inset inset')).toEqual([]); + }); + + it('should fail to parse color between lengths', () => { + expect(processBoxShadow('10px red 5px 2px 3px,')).toEqual([]); + }); + + it('should fail to parse invalid unit', () => { + expect(processBoxShadow('red 10foo 5px 2px 3px,')).toEqual([]); + }); + + it('should fail to parse decimal number with invalid unit', () => { + expect(processBoxShadow('1.5dog 2px')).toEqual([]); + }); + + it('should fail to parse invalid argument', () => { + expect(processBoxShadow('red asf 5px 2px 3px')).toEqual([]); + }); + + it('should fail to parse trailing dot without fractional digits', () => { + expect(processBoxShadow('10.px 5px')).toEqual([]); + }); + + it('should fail to parse negative blur', () => { + expect(processBoxShadow('red 5px 2px -3px')).toEqual([]); + }); + + it('should fail to parse missing unit', () => { + expect(processBoxShadow('10px 5')).toEqual([]); + }); + }); + + describe('object parsing', () => { + it('should parse simple object', () => { + expect( + processBoxShadow([ + { + offsetX: 10, + offsetY: 5, + }, + ]), + ).toEqual([ + { + offsetX: 10, + offsetY: 5, + }, + ]); + }); + + it('should parse object with color', () => { + expect( + processBoxShadow([ + { + offsetX: 10, + offsetY: 5, + color: 'red', + }, + ]), + ).toEqual([ + { + offsetX: 10, + offsetY: 5, + color: processColor('red'), + }, + ]); + }); + + it('should parse complex box shadow', () => { + expect( + processBoxShadow([ + { + offsetX: '10px', + offsetY: 5, + blurRadius: 2, + spreadDistance: 3, + inset: true, + color: '#FFFFFF', + }, + ]), + ).toEqual([ + { + offsetX: 10, + offsetY: 5, + blurRadius: 2, + spreadDistance: 3, + inset: true, + color: processColor('#FFFFFF'), + }, + ]); + }); + + it('should fail to parse object with negative blur', () => { + expect( + processBoxShadow([ + { + offsetX: 10, + offsetY: 5, + color: 'red', + blurRadius: -3, + }, + ]), + ).toEqual([]); + }); + + it('should fail to parse object with invalid argument', () => { + expect( + processBoxShadow([ + { + offsetX: 10, + offsetY: 'asdf', + }, + ]), + ).toEqual([]); + }); + + it('should fail to parse object with unitless non-zero string', () => { + expect( + processBoxShadow([ + { + offsetX: '5', + offsetY: 10, + }, + ]), + ).toEqual([]); + }); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js deleted file mode 100644 index fb9516041a4f..000000000000 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBoxShadow-test.js +++ /dev/null @@ -1,346 +0,0 @@ -/** - * 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 processBoxShadow from '../processBoxShadow'; -import processColor from '../processColor'; - -// js1 test processBoxShadow -describe('processBoxShadow', () => { - it('should parse basic string', () => { - expect(processBoxShadow('10px 5px')).toEqual([ - { - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse basic string with unitless zero length', () => { - expect(processBoxShadow('10px 0')).toEqual([ - { - offsetX: 10, - offsetY: 0, - }, - ]); - }); - - it('should parse basic string with multiple whitespaces', () => { - expect(processBoxShadow('10px 5px')).toEqual([ - { - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse string with color', () => { - expect(processBoxShadow('red 10px 5px')).toEqual([ - { - color: processColor('red'), - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse string with color function rgba', () => { - expect(processBoxShadow('rgba(255, 255, 255, 0.5) 10px 5px')).toEqual([ - { - color: processColor('rgba(255, 255, 255, 0.5)'), - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse string with color function hsl', () => { - expect(processBoxShadow('hsl(318, 69%, 55%) 10px 5px')).toEqual([ - { - color: processColor('hsl(318, 69%, 55%)'), - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse string with hex color', () => { - expect(processBoxShadow('#FFFFFF 10px 5px')).toEqual([ - { - color: processColor('#FFFFFF'), - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse string with blurRadius', () => { - expect(processBoxShadow('red 10px 5px 2px')).toEqual([ - { - color: processColor('red'), - blurRadius: 2, - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse string with spreadDistance', () => { - expect(processBoxShadow('red 10px 5px 2px 3px')).toEqual([ - { - color: processColor('red'), - blurRadius: 2, - offsetX: 10, - offsetY: 5, - spreadDistance: 3, - }, - ]); - }); - - it('should parse string arguments with units', () => { - expect(processBoxShadow('5px 2px')).toEqual([ - { - offsetX: 5, - offsetY: 2, - }, - ]); - }); - - it('should parse string with inset', () => { - expect(processBoxShadow('5px 2px inset')).toEqual([ - { - offsetX: 5, - offsetY: 2, - inset: true, - }, - ]); - }); - - it('should parse string with inset and color before and after lengths', () => { - expect(processBoxShadow('red 10px 10px inset')).toEqual([ - { - color: processColor('red'), - offsetX: 10, - offsetY: 10, - inset: true, - }, - ]); - }); - - it('should parse multiple box shadow strings', () => { - expect( - processBoxShadow( - '10px 5px red, 5px 12px inset, inset 10px 45px 13px red', - ), - ).toEqual([ - { - offsetX: 10, - offsetY: 5, - color: processColor('red'), - }, - { - offsetX: 5, - offsetY: 12, - inset: true, - }, - { - offsetX: 10, - offsetY: 45, - blurRadius: 13, - inset: true, - color: processColor('red'), - }, - ]); - }); - - it('should parse multiple box shadow strings with newlines', () => { - expect( - processBoxShadow( - '10px 5px red, 5px 12px inset,\n inset 10px 45px 13px red', - ), - ).toEqual([ - { - offsetX: 10, - offsetY: 5, - color: processColor('red'), - }, - { - offsetX: 5, - offsetY: 12, - inset: true, - }, - { - offsetX: 10, - offsetY: 45, - blurRadius: 13, - inset: true, - color: processColor('red'), - }, - ]); - }); - - it('should fail to parse string with invalid units', () => { - expect(processBoxShadow('red 10em 5$ 2| 3rp')).toEqual([]); - }); - - it('should fail to parse too many lengths', () => { - expect(processBoxShadow('10px 5px 2px 3px 10px 10px')).toEqual([]); - }); - - it('should fail to parse inset between lengths', () => { - expect(processBoxShadow('10px inset 5px 2px 3px,')).toEqual([]); - }); - - it('should fail to parse double color', () => { - expect(processBoxShadow('red red 10px 5px')).toEqual([]); - }); - - it('should fail to parse double inset', () => { - expect(processBoxShadow('10px 5px inset inset')).toEqual([]); - }); - - it('should fail to parse color between lengths', () => { - expect(processBoxShadow('10px red 5px 2px 3px,')).toEqual([]); - }); - - it('should fail to parse invalid unit', () => { - expect(processBoxShadow('red 10foo 5px 2px 3px,')).toEqual([]); - }); - - it('should fail to parse decimal number with invalid unit', () => { - expect(processBoxShadow('1.5dog 2px')).toEqual([]); - }); - - it('should fail to parse invalid argument', () => { - expect(processBoxShadow('red asf 5px 2px 3px')).toEqual([]); - }); - - it('should parse string with leading dot decimal', () => { - expect(processBoxShadow('.5px 2px')).toEqual([ - { - offsetX: 0.5, - offsetY: 2, - }, - ]); - }); - - it('should parse string with explicit positive sign', () => { - expect(processBoxShadow('+5px 2px')).toEqual([ - { - offsetX: 5, - offsetY: 2, - }, - ]); - }); - - it('should fail to parse trailing dot without fractional digits', () => { - expect(processBoxShadow('10.px 5px')).toEqual([]); - }); - - it('should fail to parse negative blur', () => { - expect(processBoxShadow('red 5px 2px -3px')).toEqual([]); - }); - - it('should fail to parse missing unit', () => { - expect(processBoxShadow('10px 5')).toEqual([]); - }); - - it('should parse simple object', () => { - expect( - processBoxShadow([ - { - offsetX: 10, - offsetY: 5, - }, - ]), - ).toEqual([ - { - offsetX: 10, - offsetY: 5, - }, - ]); - }); - - it('should parse object with color', () => { - expect( - processBoxShadow([ - { - offsetX: 10, - offsetY: 5, - color: 'red', - }, - ]), - ).toEqual([ - { - offsetX: 10, - offsetY: 5, - color: processColor('red'), - }, - ]); - }); - - it('should parse complex box shadow', () => { - expect( - processBoxShadow([ - { - offsetX: '10px', - offsetY: 5, - blurRadius: 2, - spreadDistance: 3, - inset: true, - color: '#FFFFFF', - }, - ]), - ).toEqual([ - { - offsetX: 10, - offsetY: 5, - blurRadius: 2, - spreadDistance: 3, - inset: true, - color: processColor('#FFFFFF'), - }, - ]); - }); - - it('should fail to parse object with negative blur', () => { - expect( - processBoxShadow([ - { - offsetX: 10, - offsetY: 5, - color: 'red', - blurRadius: -3, - }, - ]), - ).toEqual([]); - }); - - it('should fail to parse object with invalid argument', () => { - expect( - processBoxShadow([ - { - offsetX: 10, - offsetY: 'asdf', - }, - ]), - ).toEqual([]); - }); - - it('should fail to parse object with unitless non-zero string', () => { - expect( - processBoxShadow([ - { - offsetX: '5', - offsetY: 10, - }, - ]), - ).toEqual([]); - }); -});