Skip to content

Replace custom brace expansion with brace-expansion library for faster pattern matching#19824

Closed
marco-carvalho wants to merge 1 commit intotailwindlabs:mainfrom
marco-carvalho:replace-custom-brace-expansion
Closed

Replace custom brace expansion with brace-expansion library for faster pattern matching#19824
marco-carvalho wants to merge 1 commit intotailwindlabs:mainfrom
marco-carvalho:replace-custom-brace-expansion

Conversation

@marco-carvalho
Copy link
Contributor

Summary

Replace custom brace expansion with brace-expansion library for faster pattern matching

Why:

  • 2x faster on the project's own benchmark pattern (1.49M expansions: ~72ms → ~24ms).
  • Less code to maintain — 90 lines of custom parsing reduced to a 10-line wrapper with a single guard for step=0 (which causes an infinite loop in the library).
  • Built-in DoS protection — the library caps expansions at 100,000 by default, which the custom implementation did not.
  • Zero-padding support{001..9} now correctly produces 001, 002, …, 009 instead of stripping leading zeros.

Behavioral changes:

Behavior Before After
Zero-padding ({00..05}) Stripped → 0, 1, … Preserved → 00, 01, …
Decimal ranges ({1.1..2.2}) 1.1..2.2 {1.1..2.2} (braces preserved as literal)
Unbalanced braces Throws Error Graceful degradation (unmatched braces kept as literal text)
Step=0 ({0..5..0}) Throws Error Still throws (wrapper guard)

Test plan

  • Ran the existing brace-expansion unit tests with updated expectations:
    cd packages/tailwindcss && npx vitest run src/utils/brace-expansion.test.ts
    # 22 tests passed
    
  • Ran the full tailwindcss test suite to verify no regressions:
    cd packages/tailwindcss && npx vitest run
    # 4,117 tests passed | 1 skipped
    
  • Benchmarked before/after on the project's own large expansion pattern (1.49M items):
    Before (custom):            mean 72.40ms, 13.8 ops/s
    After  (brace-expansion):   mean 24.27ms, 41.2 ops/s
    Speedup:                    2.1x
    

@marco-carvalho marco-carvalho requested a review from a team as a code owner March 18, 2026 18:10
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 808ca7d7-86d8-40d6-851c-bb2e39d1e3ac

📥 Commits

Reviewing files that changed from the base of the PR and between d596b0c and 1d6a61a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/tailwindcss/package.json
  • packages/tailwindcss/src/utils/brace-expansion.test.ts
  • packages/tailwindcss/src/utils/brace-expansion.ts

Walkthrough

This pull request replaces the custom brace expansion implementation in packages/tailwindcss/src/utils/brace-expansion.ts with an external library dependency. The brace-expansion library (version ^5.0.4) is added as a devDependency in packages/tailwindcss/package.json. The public function signature expand(pattern: string): string[] remains unchanged. Internal parsing, sequencing, and recursion logic are removed, with expansion now delegated to the external library. Test expectations are updated to reflect the library's behavior, including zero-padding support in numeric ranges, decimal range handling, and graceful processing of unbalanced braces.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing custom brace expansion logic with an external library for improved performance.
Description check ✅ Passed The description is well-related to the changeset, explaining the rationale (performance, maintenance, DoS protection, zero-padding), behavioral changes, and comprehensive test results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

@marco-carvalho marco-carvalho marked this pull request as draft March 18, 2026 18:27
@RobinMalfait
Copy link
Member

Hey! What kind of issues were you running into that triggered this PR?

Brace expansion is only used for our @source inline('…') feature (https://tailwindcss.com/docs/detecting-classes-in-source-files#safelisting-variants) and we explicitly don't support certain features because they don't make sense in a Tailwind CSS class context.

For example:

  • Zero-padding support: we don't support utilities like w-0001, so it doesn't make sense for us to support this
  • Decimal ranges: similar to decimal ranges, we support multiples of .25, but w-1.2 is not supported, so this also doesn't really make sense here.
  • Unbalanced braces: this throws on purpose to inform the user that something is wrong
  • Built-in DoS protection: Tailwind CSS is not executed at runtime, and the @source inline(…) feature doesn't handle arbitrary inputs except from the hardcoded list in the CSS. Worst case if you manage to get it to expand forever, your memory goes up and/or your dev process crashes. A production website will not be affected.

So I'm really curious about the issues you were running into.

@marco-carvalho
Copy link
Contributor Author

hi @RobinMalfait there wasn't a specific issue, just a codebase exploration for maintenance wins, but your points are well taken, so happy to close this.

@RobinMalfait
Copy link
Member

No worries, was just interested in what caused this. You did peak my interest with the 2x faster part, so wanted to see where we can make things faster even if this is not a hot-path code. But first, I wanted to gather some data:

On a small pattern like: {text,bg}-{red,green,blue}-{50,{100..900..100},950}, there will be 66 generated classes. In this scenario, the braces-expansion package is ~4.6 times slower than the current implementation:
image

On a large pattern like: {{xs,sm,md,lg}:,}{border-{x,y,t,r,b,l,s,e},bg,text,cursor,accent}-{{red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate,gray,zinc,neutral,stone}-{50,{100..900..100},950},black,white}{,/{0..100}}, there will be 1 493 280 generated classes.

It looked like in this scenario the braces-expansion packages is almost ~2x faster, but that's only because it does indeed cap out at 100k generated classes:
image

If we increase that limit to a much bigger number, the braces-expansion package is ~10x slower compared to the current implementation:
image

Here is the exact code I used for the benchmark:

import { expand as braceExpansionExpand } from 'brace-expansion'
import { expand as bracesExpand } from 'braces'
import { expand } from './brace-expansion'

import { bench, boxplot, do_not_optimize, run, summary } from 'mitata'

const PATTERN =
  '{{xs,sm,md,lg}:,}{border-{x,y,t,r,b,l,s,e},bg,text,cursor,accent}-{{red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate,gray,zinc,neutral,stone}-{50,{100..900..100},950},black,white}{,/{0..100}}'
// const PATTERN = '{text,bg}-{red,green,blue}-{50,{100..900..100},950}'
const braceExpansionExpandOptions = {
  max: 1_500_000,
}

// Verify that the results are the same (at least length wise)
let a = bracesExpand(PATTERN)
let b = braceExpansionExpand(PATTERN, braceExpansionExpandOptions)
let c = expand(PATTERN)

console.log('Results', a.length, b.length, c.length)

summary(() => {
  boxplot(() => {
    bench('npm: `braces`', () => {
      do_not_optimize(bracesExpand(PATTERN))
    })

    bench('npm: `braces-expansion`', () => {
      do_not_optimize(braceExpansionExpand(PATTERN, braceExpansionExpandOptions))
    })

    bench('local: ./brace-expansion', () => {
      do_not_optimize(expand(PATTERN))
    })
  })
})

await run()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants