Skip to content

Commit 934ba4b

Browse files
sammy-SCfacebook-github-bot
authored andcommitted
Hoist regex patterns to module-level in StyleSheet processors
Summary: changelog: [internal] Hoist inline regex patterns to module-level constants in processFilter.js, processBackgroundImage.js, and processTransformOrigin.js to avoid repeated regex compilation on each function call. Fantom benchmark results (p50 latency, optimized build). | Benchmark | Before | After | Improvement | |---|---|---|---| | 100 views with filter strings | 4.427ms | 3.556ms | 24.48% faster | | 500 views with filter strings | 22.523ms | 17.861ms | 26.10% faster | | 100 views with boxShadow strings | 5.835ms | 5.729ms | 1.85% faster | | 500 views with boxShadow strings | 29.914ms | 28.846ms | 3.70% faster | | 100 views with transformOrigin strings | 2.521ms | 2.512ms | 0.36% faster | | 500 views with transformOrigin strings | 12.706ms | 12.421ms | 2.30% faster | Reviewed By: javache Differential Revision: D95853070
1 parent d527f2d commit 934ba4b

3 files changed

Lines changed: 45 additions & 20 deletions

File tree

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import type {
2020

2121
const processColor = require('./processColor').default;
2222

23+
// Pre-compiled regex patterns for performance - avoids regex compilation on each call
24+
const NEWLINE_REGEX = /\n/g;
25+
const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
26+
const COMMA_SPLIT_REGEX = /,(?![^(]*\))/;
27+
const WHITESPACE_SPLIT_REGEX = /\s+/;
28+
const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
29+
const WHITESPACE_NORMALIZE_REGEX = /\s+/g;
30+
2331
// Linear Gradient
2432
const LINEAR_GRADIENT_DIRECTION_REGEX =
2533
/^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/i;
@@ -81,7 +89,9 @@ export default function processBackgroundImage(
8189
}
8290

8391
if (typeof backgroundImage === 'string') {
84-
result = parseBackgroundImageCSSString(backgroundImage.replace(/\n/g, ' '));
92+
result = parseBackgroundImageCSSString(
93+
backgroundImage.replace(NEWLINE_REGEX, ' '),
94+
);
8595
} else if (Array.isArray(backgroundImage)) {
8696
for (const bgImage of backgroundImage) {
8797
const processedColorStops = processColorStops(bgImage);
@@ -255,9 +265,7 @@ function parseBackgroundImageCSSString(
255265

256266
for (const bgImageString of bgImageStrings) {
257267
const bgImage = bgImageString.toLowerCase();
258-
const gradientRegex = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
259-
260-
const match = gradientRegex.exec(bgImage);
268+
const match = GRADIENT_REGEX.exec(bgImage);
261269
if (match) {
262270
const [, type, gradientContent] = match;
263271
const isRadial = type.toLowerCase() === 'radial';
@@ -281,15 +289,15 @@ function parseRadialGradientCSSString(
281289
let position: RadialGradientPosition = {...DEFAULT_RADIAL_POSITION};
282290

283291
// split the content by commas, but not if inside parentheses (for color values)
284-
const parts = gradientContent.split(/,(?![^(]*\))/);
292+
const parts = gradientContent.split(COMMA_SPLIT_REGEX);
285293
// first part may contain shape, size, and position
286294
// [ <radial-shape> || <radial-size> ]? [ at <position> ]?
287295
const firstPartStr = parts[0].trim();
288296
const remainingParts = [...parts];
289297
let hasShapeSizeOrPositionString = false;
290298
let hasExplicitSingleSize = false;
291299
let hasExplicitShape = false;
292-
const firstPartTokens = firstPartStr.split(/\s+/);
300+
const firstPartTokens = firstPartStr.split(WHITESPACE_SPLIT_REGEX);
293301

294302
// firstPartTokens is the shape, size, and position
295303
while (firstPartTokens.length > 0) {
@@ -626,13 +634,13 @@ function parseColorStopsCSSString(parts: Array<string>): Array<{
626634
position: ColorStopPosition,
627635
}> = [];
628636
// split by comma, but not if it's inside a parentheses. e.g. red, rgba(0, 0, 0, 0.5), green => ["red", "rgba(0, 0, 0, 0.5)", "green"]
629-
const stops = colorStopsString.split(/,(?![^(]*\))/);
637+
const stops = colorStopsString.split(COMMA_SPLIT_REGEX);
630638
let prevStop = null;
631639
for (let i = 0; i < stops.length; i++) {
632640
const stop = stops[i];
633641
const trimmedStop = stop.trim().toLowerCase();
634642
// Match function like pattern or single words
635-
const colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g);
643+
const colorStopParts = trimmedStop.match(COLOR_STOP_PARTS_REGEX);
636644
if (colorStopParts == null) {
637645
// If a color stop is invalid, return null and do not apply any gradient. Same as web.
638646
return null;
@@ -726,7 +734,9 @@ function getDirectionForKeyword(direction?: string): ?LinearGradientDirection {
726734
return null;
727735
}
728736
// Remove extra whitespace
729-
const normalized = direction.replace(/\s+/g, ' ').toLowerCase();
737+
const normalized = direction
738+
.replace(WHITESPACE_NORMALIZE_REGEX, ' ')
739+
.toLowerCase();
730740

731741
switch (normalized) {
732742
case 'to top':

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ import type {DropShadowValue, FilterFunction} from './StyleSheetTypes';
1515

1616
import processColor from './processColor';
1717

18+
// Pre-compiled regex patterns for performance - avoids regex compilation on each call
19+
const NEWLINE_REGEX = /\n/g;
20+
const FILTER_FUNCTION_REGEX =
21+
/([\w-]+)\(([^()]*|\([^()]*\)|[^()]*\([^()]*\)[^()]*)\)/g;
22+
const ARGS_WITH_UNITS_REGEX = /([+-]?\d*(\.\d+)?)([a-zA-Z%]+)?/g;
23+
const WHITESPACE_SPLIT_REGEX = /\s+(?![^(]*\))/;
24+
const LENGTH_PARSE_REGEX = /([+-]?\d*(\.\d+)?)([\w\W]+)?/g;
25+
1826
type ParsedFilter =
1927
| {brightness: number}
2028
| {blur: number}
@@ -43,13 +51,13 @@ export default function processFilter(
4351
}
4452

4553
if (typeof filter === 'string') {
46-
filter = filter.replace(/\n/g, ' ');
54+
filter = filter.replace(NEWLINE_REGEX, ' ');
4755

4856
// matches on functions with args and nested functions like "drop-shadow(10 10 10 rgba(0, 0, 0, 1))"
49-
const regex = /([\w-]+)\(([^()]*|\([^()]*\)|[^()]*\([^()]*\)[^()]*)\)/g;
57+
FILTER_FUNCTION_REGEX.lastIndex = 0;
5058
let matches;
5159

52-
while ((matches = regex.exec(filter))) {
60+
while ((matches = FILTER_FUNCTION_REGEX.exec(filter))) {
5361
let filterName = matches[1].toLowerCase();
5462
if (filterName === 'drop-shadow') {
5563
const dropShadow = parseDropShadow(matches[2]);
@@ -120,8 +128,8 @@ function _getFilterAmount(filterName: string, filterArgs: unknown): ?number {
120128
let unit: string;
121129
if (typeof filterArgs === 'string') {
122130
// matches on args with units like "1.5 5% -80deg"
123-
const argsWithUnitsRegex = new RegExp(/([+-]?\d*(\.\d+)?)([a-zA-Z%]+)?/g);
124-
const match = argsWithUnitsRegex.exec(filterArgs);
131+
ARGS_WITH_UNITS_REGEX.lastIndex = 0;
132+
const match = ARGS_WITH_UNITS_REGEX.exec(filterArgs);
125133

126134
if (!match || isNaN(Number(match[1]))) {
127135
return undefined;
@@ -258,7 +266,7 @@ function parseDropShadowString(rawDropShadow: string): ?DropShadowValue {
258266
let keywordDetectedAfterLength = false;
259267

260268
// split args by all whitespaces that are not in parenthesis
261-
for (const arg of rawDropShadow.split(/\s+(?![^(]*\))/)) {
269+
for (const arg of rawDropShadow.split(WHITESPACE_SPLIT_REGEX)) {
262270
const processedColor = processColor(arg);
263271
if (processedColor != null) {
264272
if (dropShadow.color != null) {
@@ -305,8 +313,8 @@ function parseDropShadowString(rawDropShadow: string): ?DropShadowValue {
305313

306314
function parseLength(length: string): ?number {
307315
// matches on args with units like "1.5 5% -80deg"
308-
const argsWithUnitsRegex = /([+-]?\d*(\.\d+)?)([\w\W]+)?/g;
309-
const match = argsWithUnitsRegex.exec(length);
316+
LENGTH_PARSE_REGEX.lastIndex = 0;
317+
const match = LENGTH_PARSE_REGEX.exec(length);
310318

311319
if (!match || Number.isNaN(match[1])) {
312320
return null;

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
import invariant from 'invariant';
1212

13+
// Pre-compiled regex pattern for performance - avoids regex compilation on each call
14+
const TRANSFORM_ORIGIN_REGEX = /(top|bottom|left|right|center|\d+(?:%|px)|0)/gi;
15+
1316
const INDEX_X = 0;
1417
const INDEX_Y = 1;
1518
const INDEX_Z = 2;
@@ -20,12 +23,14 @@ export default function processTransformOrigin(
2023
): Array<string | number> {
2124
if (typeof transformOrigin === 'string') {
2225
const transformOriginString = transformOrigin;
23-
const regex = /(top|bottom|left|right|center|\d+(?:%|px)|0)/gi;
26+
TRANSFORM_ORIGIN_REGEX.lastIndex = 0;
2427
const transformOriginArray: Array<string | number> = ['50%', '50%', 0];
2528

2629
let index = INDEX_X;
2730
let matches;
28-
outer: while ((matches = regex.exec(transformOriginString))) {
31+
outer: while (
32+
(matches = TRANSFORM_ORIGIN_REGEX.exec(transformOriginString))
33+
) {
2934
let nextIndex = index + 1;
3035

3136
const value = matches[0];
@@ -53,7 +58,9 @@ export default function processTransformOrigin(
5358

5459
// Handle [[ center | left | right ] && [ center | top | bottom ]] <length>?
5560
if (index === INDEX_X) {
56-
const horizontal = regex.exec(transformOriginString);
61+
const horizontal = TRANSFORM_ORIGIN_REGEX.exec(
62+
transformOriginString,
63+
);
5764
if (horizontal == null) {
5865
break outer;
5966
}

0 commit comments

Comments
 (0)