Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions src/lib/class-group-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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++) {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/merge-classlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions src/lib/sort-modifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []

Expand All @@ -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 = []
}
Expand All @@ -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)
}

Expand Down
29 changes: 29 additions & 0 deletions tests/tw-merge.benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({})
Expand Down Expand Up @@ -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(() => {
Expand Down
Loading