diff --git a/src/lib/class-group-utils.ts b/src/lib/class-group-utils.ts index 2a6afca1..680ef393 100644 --- a/src/lib/class-group-utils.ts +++ b/src/lib/class-group-utils.ts @@ -57,26 +57,32 @@ export const createClassGroupUtils = (config: AnyConfig) => { } const classParts = className.split(CLASS_PART_SEPARATOR) - // Classes like `-inset-1` produce an empty string as first classPart. We assume that classes for negative values are used correctly and remove it from classParts. - if (classParts[0] === '' && classParts.length > 1) { - classParts.shift() - } - return getGroupRecursive(classParts, classMap) + // Classes like `-inset-1` produce an empty string as first classPart. We assume that classes for negative values are used correctly and skip it. + const startIndex = classParts[0] === '' && classParts.length > 1 ? 1 : 0 + return getGroupRecursive(classParts, startIndex, classMap) } const getConflictingClassGroupIds = ( classGroupId: AnyClassGroupIds, hasPostfixModifier: boolean, ): readonly AnyClassGroupIds[] => { - const conflicts = conflictingClassGroups[classGroupId] - if (!hasPostfixModifier || !conflictingClassGroupModifiers[classGroupId]) { - return conflicts || EMPTY_CONFLICTS + if (hasPostfixModifier) { + const modifierConflicts = conflictingClassGroupModifiers[classGroupId] + const baseConflicts = conflictingClassGroups[classGroupId] + + if (modifierConflicts) { + if (baseConflicts) { + // Merge base conflicts with modifier conflicts + return concatArrays(baseConflicts, modifierConflicts) + } + // Only modifier conflicts + return modifierConflicts + } + // Fall back to without postfix if no modifier conflicts + return baseConflicts || EMPTY_CONFLICTS } - const modifierConflicts = conflictingClassGroupModifiers[classGroupId]! - if (!conflicts) return modifierConflicts - - return concatArrays(conflicts, modifierConflicts) + return conflictingClassGroups[classGroupId] || EMPTY_CONFLICTS } return { @@ -87,18 +93,19 @@ export const createClassGroupUtils = (config: AnyConfig) => { const getGroupRecursive = ( classParts: string[], + startIndex: number, classPartObject: ClassPartObject, ): AnyClassGroupIds | undefined => { - const classPathsLength = classParts.length + const classPathsLength = classParts.length - startIndex if (classPathsLength === 0) { return classPartObject.classGroupId } - const currentClassPart = classParts[0]! + const currentClassPart = classParts[startIndex]! const nextClassPartObject = classPartObject.nextPart.get(currentClassPart) if (nextClassPartObject) { - const result = getGroupRecursive(classParts.slice(1), nextClassPartObject) + const result = getGroupRecursive(classParts, startIndex + 1, nextClassPartObject) if (result) return result } @@ -107,7 +114,11 @@ const getGroupRecursive = ( return undefined } - const classRest = classParts.join(CLASS_PART_SEPARATOR) + // Build classRest string efficiently by joining from startIndex onwards + const classRest = + startIndex === 0 + ? classParts.join(CLASS_PART_SEPARATOR) + : classParts.slice(startIndex).join(CLASS_PART_SEPARATOR) const validatorsLength = validators.length for (let i = 0; i < validatorsLength; i++) { diff --git a/src/lib/merge-classlist.ts b/src/lib/merge-classlist.ts index 4b4f5cc2..886b8bc1 100644 --- a/src/lib/merge-classlist.ts +++ b/src/lib/merge-classlist.ts @@ -60,7 +60,13 @@ export const mergeClassList = (classList: string, configUtils: ConfigUtils) => { hasPostfixModifier = false } - const variantModifier = sortModifiers(modifiers).join(':') + // Fast path: skip sorting for empty or single modifier + const variantModifier = + modifiers.length === 0 + ? '' + : modifiers.length === 1 + ? modifiers[0]! + : sortModifiers(modifiers).join(':') const modifierId = hasImportantModifier ? variantModifier + IMPORTANT_MODIFIER diff --git a/src/lib/sort-modifiers.ts b/src/lib/sort-modifiers.ts index 708274ac..caf3b614 100644 --- a/src/lib/sort-modifiers.ts +++ b/src/lib/sort-modifiers.ts @@ -15,9 +15,6 @@ export const createSortModifiers = (config: AnyConfig) => { }) return (modifiers: readonly string[]): string[] => { - // Fast path for common cases - if (modifiers.length <= 1) return [...modifiers] - const result: string[] = [] let currentSegment: string[] = [] @@ -32,7 +29,7 @@ export const createSortModifiers = (config: AnyConfig) => { if (isArbitrary || isOrderSensitive) { // Sort and flush current segment alphabetically if (currentSegment.length > 0) { - currentSegment.sort((a, b) => a.localeCompare(b)) + currentSegment.sort() result.push(...currentSegment) currentSegment = [] } @@ -45,7 +42,7 @@ export const createSortModifiers = (config: AnyConfig) => { // Sort and add any remaining segment items if (currentSegment.length > 0) { - currentSegment.sort((a, b) => a.localeCompare(b)) + currentSegment.sort() result.push(...currentSegment) } diff --git a/tests/tw-merge.benchmark.ts b/tests/tw-merge.benchmark.ts index ccd4f8ea..4d582382 100644 --- a/tests/tw-merge.benchmark.ts +++ b/tests/tw-merge.benchmark.ts @@ -4,6 +4,23 @@ import { extendTailwindMerge } from '../src' import testDataCollection from './tw-merge-benchmark-data.json' +// Create a long class list with many conflicts (padding, margin, width, height, etc.) +const ultraLongClassList: string[] = [] +for (let i = 0; i < 200; i++) { + // Add padding classes that conflict + ultraLongClassList.push(`p-${i % 20}`, `px-${i % 20}`, `py-${i % 20}`) + // Add margin classes that conflict + ultraLongClassList.push(`m-${i % 20}`, `mx-${i % 20}`, `my-${i % 20}`) + // Add width/height classes that conflict + ultraLongClassList.push(`w-${i % 20}`, `h-${i % 20}`) + // Add some non-conflicting classes + ultraLongClassList.push(`text-${i % 10}`, `bg-${i % 10}`) + // Add some with modifiers + if (i % 10 === 0) { + ultraLongClassList.push(`hover:p-${i % 20}`, `focus:m-${i % 20}`) + } +} + describe('twMerge', () => { benchWithMemory('init', () => { const twMerge = extendTailwindMerge({}) @@ -49,6 +66,18 @@ describe('twMerge', () => { twMerge(...(testDataCollection[index] as TestDataItem)) } }) + + benchWithMemory('ultra long class list with many conflicts without cache', () => { + const twMerge = extendTailwindMerge({ cacheSize: 0 }) + + twMerge(...ultraLongClassList) + }) + + benchWithMemory('ultra long class list with many conflicts with cache', () => { + const twMerge = extendTailwindMerge({}) + + twMerge(...ultraLongClassList) + }) }) afterAll(() => {