diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 13c3bc257a91..80fc17bda7e8 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1099,10 +1099,14 @@ function allVariablesAreUsed( walk(ValueParser.parse(value), (node) => { if (node.kind === 'function' && node.value === 'var') { let variable = node.nodes[0].value - let r = new RegExp(`var\\(${variable}[,)]\\s*`, 'g') + // Check if the variable is used in the replacement using string methods + // instead of RegExp to avoid the overhead of creating a RegExp per variable + let hasVarWithComma = replacementAsCss.includes(`var(${variable},`) + let hasVarWithParen = replacementAsCss.includes(`var(${variable})`) + if ( // We need to check if the variable is used in the replacement - !r.test(replacementAsCss) || + !(hasVarWithComma || hasVarWithParen) || // The value cannot be set to a different value in the // replacement because that would make it an unsafe migration replacementAsCss.includes(`${variable}:`) diff --git a/packages/tailwindcss/src/utils/brace-expansion.ts b/packages/tailwindcss/src/utils/brace-expansion.ts index ee382cd82a29..ec53ec447fc7 100644 --- a/packages/tailwindcss/src/utils/brace-expansion.ts +++ b/packages/tailwindcss/src/utils/brace-expansion.ts @@ -68,24 +68,25 @@ function expandSequence(seq: string): string[] { let step = stepStr ? parseInt(stepStr, 10) : undefined let result: string[] = [] - if (/^-?\d+$/.test(start) && /^-?\d+$/.test(end)) { - let startNum = parseInt(start, 10) - let endNum = parseInt(end, 10) + // NUMERICAL_RANGE already ensures start and end are numbers (-?\d+) + // so no need to test again + let startNum = parseInt(start, 10) + let endNum = parseInt(end, 10) - if (step === undefined) { - step = startNum <= endNum ? 1 : -1 - } - if (step === 0) { - throw new Error('Step cannot be zero in sequence expansion.') - } + if (step === undefined) { + step = startNum <= endNum ? 1 : -1 + } + if (step === 0) { + throw new Error('Step cannot be zero in sequence expansion.') + } - let increasing = startNum < endNum - if (increasing && step < 0) step = -step - if (!increasing && step > 0) step = -step + let increasing = startNum < endNum + if (increasing && step < 0) step = -step + if (!increasing && step > 0) step = -step - for (let i = startNum; increasing ? i <= endNum : i >= endNum; i += step) { - result.push(i.toString()) - } + for (let i = startNum; increasing ? i <= endNum : i >= endNum; i += step) { + result.push(i.toString()) } + return result } diff --git a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts index 145103188131..1130df53ab44 100644 --- a/packages/tailwindcss/src/utils/decode-arbitrary-value.ts +++ b/packages/tailwindcss/src/utils/decode-arbitrary-value.ts @@ -21,6 +21,11 @@ export function decodeArbitraryValue(input: string): string { * converted to `_` instead. */ function convertUnderscoresToWhitespace(input: string, skipUnderscoreToSpace = false) { + // Fast path: if there are no underscores at all, return input as-is + if (input.indexOf('_') === -1) { + return input + } + let output = '' for (let i = 0; i < input.length; i++) { let char = input[i] diff --git a/packages/tailwindcss/src/utils/infer-data-type.ts b/packages/tailwindcss/src/utils/infer-data-type.ts index 587bae785c79..c7b2c79a981b 100644 --- a/packages/tailwindcss/src/utils/infer-data-type.ts +++ b/packages/tailwindcss/src/utils/infer-data-type.ts @@ -66,14 +66,11 @@ function isUrl(value: string): boolean { /* -------------------------------------------------------------------------- */ +const LINE_WIDTH_KEYWORDS = new Set(['thin', 'medium', 'thick']) + function isLineWidth(value: string): boolean { return segment(value, ' ').every( - (value) => - isLength(value) || - isNumber(value) || - value === 'thin' || - value === 'medium' || - value === 'thick', + (value) => isLength(value) || isNumber(value) || LINE_WIDTH_KEYWORDS.has(value), ) } @@ -111,22 +108,24 @@ function isImage(value: string) { /* -------------------------------------------------------------------------- */ +const GENERIC_NAMES = new Set([ + 'serif', + 'sans-serif', + 'monospace', + 'cursive', + 'fantasy', + 'system-ui', + 'ui-serif', + 'ui-sans-serif', + 'ui-monospace', + 'ui-rounded', + 'math', + 'emoji', + 'fangsong', +]) + function isGenericName(value: string): boolean { - return ( - value === 'serif' || - value === 'sans-serif' || - value === 'monospace' || - value === 'cursive' || - value === 'fantasy' || - value === 'system-ui' || - value === 'ui-serif' || - value === 'ui-sans-serif' || - value === 'ui-monospace' || - value === 'ui-rounded' || - value === 'math' || - value === 'emoji' || - value === 'fangsong' - ) + return GENERIC_NAMES.has(value) } function isFamilyName(value: string): boolean { @@ -145,17 +144,19 @@ function isFamilyName(value: string): boolean { return count > 0 } +const ABSOLUTE_SIZES = new Set([ + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large', + 'xxx-large', +]) + function isAbsoluteSize(value: string): boolean { - return ( - value === 'xx-small' || - value === 'x-small' || - value === 'small' || - value === 'medium' || - value === 'large' || - value === 'x-large' || - value === 'xx-large' || - value === 'xxx-large' - ) + return ABSOLUTE_SIZES.has(value) } function isRelativeSize(value: string): boolean { @@ -239,17 +240,13 @@ export function isLength(value: string): boolean { /* -------------------------------------------------------------------------- */ +const BACKGROUND_POSITION_KEYWORDS = new Set(['center', 'top', 'right', 'bottom', 'left']) + function isBackgroundPosition(value: string): boolean { let count = 0 for (let part of segment(value, ' ')) { - if ( - part === 'center' || - part === 'top' || - part === 'right' || - part === 'bottom' || - part === 'left' - ) { + if (BACKGROUND_POSITION_KEYWORDS.has(part)) { count += 1 continue } diff --git a/packages/tailwindcss/src/utils/is-color.ts b/packages/tailwindcss/src/utils/is-color.ts index e3501aa8673d..86e2e0ca7f64 100644 --- a/packages/tailwindcss/src/utils/is-color.ts +++ b/packages/tailwindcss/src/utils/is-color.ts @@ -198,7 +198,26 @@ const NAMED_COLORS = new Set([ const IS_COLOR_FN = /^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix)\(/i export function isColor(value: string): boolean { - return ( - value.charCodeAt(0) === HASH || IS_COLOR_FN.test(value) || NAMED_COLORS.has(value.toLowerCase()) - ) + // Fast path: check for hash first (hex colors) + if (value.charCodeAt(0) === HASH) return true + + // Fast path: check for color functions + if (IS_COLOR_FN.test(value)) return true + + // Check named colors - try lowercase first, then convert if needed + // Most values in practice are already lowercase + if (NAMED_COLORS.has(value)) return true + + // Only convert to lowercase if the first check failed and value contains uppercase + // This avoids the toLowerCase() call in the common case + let hasUppercase = false + for (let i = 0; i < value.length; i++) { + let code = value.charCodeAt(i) + if (code >= 65 && code <= 90) { // A-Z + hasUppercase = true + break + } + } + + return hasUppercase && NAMED_COLORS.has(value.toLowerCase()) } diff --git a/packages/tailwindcss/src/utils/math-operators.ts b/packages/tailwindcss/src/utils/math-operators.ts index d31cc3c526c9..3bde38cfbcde 100644 --- a/packages/tailwindcss/src/utils/math-operators.ts +++ b/packages/tailwindcss/src/utils/math-operators.ts @@ -39,12 +39,44 @@ const MATH_FUNCTIONS = [ ] export function hasMathFn(input: string) { - return input.indexOf('(') !== -1 && MATH_FUNCTIONS.some((fn) => input.includes(`${fn}(`)) + // Fast path: no opening paren means no function calls + if (input.indexOf('(') === -1) return false + + // Check for math functions without creating template strings + for (let fn of MATH_FUNCTIONS) { + // Build the pattern to match: "fn(" + // Look for the function name followed immediately by opening paren + let idx = 0 + while ((idx = input.indexOf(fn, idx)) !== -1) { + // Check if the character after the function name is '(' + // charCodeAt returns NaN for out-of-bounds, which won't equal OPEN_PAREN + if (input.charCodeAt(idx + fn.length) === OPEN_PAREN) { + return true + } + idx++ + } + } + + return false } export function addWhitespaceAroundMathOperators(input: string) { // Bail early if there are no math functions in the input - if (!MATH_FUNCTIONS.some((fn) => input.includes(fn))) { + // Check for opening paren first as a fast path + if (input.indexOf('(') === -1) { + return input + } + + // Check if any math function exists in the string + let hasMathFunction = false + for (let fn of MATH_FUNCTIONS) { + if (input.includes(fn)) { + hasMathFunction = true + break + } + } + + if (!hasMathFunction) { return input } diff --git a/packages/tailwindcss/src/utils/to-key-path.ts b/packages/tailwindcss/src/utils/to-key-path.ts index 40334612eea5..69072630086d 100644 --- a/packages/tailwindcss/src/utils/to-key-path.ts +++ b/packages/tailwindcss/src/utils/to-key-path.ts @@ -45,7 +45,7 @@ export function toKeyPath(path: string) { } // Add the part after the last bracket as a key - if (currentIndex <= part.length - 1) { + if (currentIndex < part.length) { keypath.push(part.slice(currentIndex)) } }