diff --git a/packages/react-native/Libraries/StyleSheet/processBoxShadow.js b/packages/react-native/Libraries/StyleSheet/processBoxShadow.js index 0d1aa710d29e..e29026fab7c6 100644 --- a/packages/react-native/Libraries/StyleSheet/processBoxShadow.js +++ b/packages/react-native/Libraries/StyleSheet/processBoxShadow.js @@ -11,8 +11,15 @@ 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; + export type ParsedBoxShadow = { offsetX: number, offsetY: number, @@ -32,7 +39,12 @@ export default function processBoxShadow( const boxShadowList = typeof rawBoxShadows === 'string' - ? parseBoxShadowString(rawBoxShadows.replace(/\n/g, ' ')) + ? parseBoxShadowString( + rawBoxShadows.replace( + enableOptimizedBoxShadowParsing() ? NEWLINE_REGEX : /\n/g, + ' ', + ), + ) : rawBoxShadows; for (const rawBoxShadow of boxShadowList) { @@ -109,7 +121,9 @@ function parseBoxShadowString(rawBoxShadows: string): Array { let result: Array = []; for (const rawBoxShadow of rawBoxShadows - .split(/,(?![^()]*\))/) // split by comma that is not in parenthesis + .split( + enableOptimizedBoxShadowParsing() ? COMMA_SPLIT_REGEX : /,(?![^()]*\))/, + ) // split by comma that is not in parenthesis .map(bS => bS.trim()) .filter(bS => bS !== '')) { const boxShadow: BoxShadowValue = { @@ -123,7 +137,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( + enableOptimizedBoxShadowParsing() + ? WHITESPACE_SPLIT_REGEX + : /\s+(?![^(]*\))/, + ); for (const arg of args) { const processedColor = processColor(arg); if (processedColor != null) { @@ -192,6 +210,28 @@ function parseBoxShadowString(rawBoxShadows: string): Array { } function parseLength(length: string): ?number { + if (enableOptimizedBoxShadowParsing()) { + // 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. */