Skip to content

refactor(theme)!: swap tailwind-merge for cn behind the merger seam - #7006

Draft
benjamincanac wants to merge 2 commits into
v5from
feat/cn-merger
Draft

benjamincanac wants to merge 2 commits into
v5from
feat/cn-merger

Conversation

@benjamincanac

@benjamincanac benjamincanac commented Sep 22, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

Part of #6918, Epic H of #6610. Needs shadcn-ui/cn#143, fixed in cn@0.3.1.

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Draft: it works and it is measured, the gzipped size is the one number that goes the wrong way. Numbers at the end.

On hold (2026-09-22). Nothing here reaches users: the merger runs 2 to 9 times on a cold render and never on a warm one. Against that, it costs 734 B gzip and the class-group extension. The seam keeps this a one-import change whenever we pick it up, and cn/config can bring class groups back without a breaking change. Worth revisiting when any of these moves:

  • cn's gzipped size after bundling is at or under tailwind-merge's (9.8 KB vs 8.5 KB today)
  • cn owns its tables instead of generating them from tailwind-merge's config
  • the engine starts calling the merger on hot paths

cn replaces tailwind-merge behind getMerger(), the seam the engine has had since #6964. Owning the engine is what makes this one import rather than aliasing a package inside the consumer's build graph.

Why now. Our report (shadcn-ui/cn#143) was fixed in cn@0.3.1: the vendored tailwind-merge went to 3.7.0, the tables were regenerated and a logical.mjs conformance suite was added. Before that px-2 did not override pe-11, which every theme here depends on since they are written with logical properties throughout.

Output is the same. 200,000 class chains joined from real theme fragments plus typical :ui overrides: 0.34% differ, all of them bg-gradient-to-* against a later bg-<color>, where cn keeps the gradient and tailwind-merge drops it. cn is right there, and #7007 already moved those themes to bg-linear-*, so the suite has zero snapshot churn.

app.config.ui.tv.mergeConfig is replaced by prefix and cacheSize, the two options that have an equivalent, tested under a mocked app config since the suite never sets them. The engine builds its merger from cn/engine and cn/tables, which the default path already pulls in, so the prefix we set from theme.prefix costs nothing extra.

Extending class groups has no equivalent and is dropped. That is what keeps cn/config, the 8 KB compiler, out of the picture: no conditional import, no generated stub, no restart caveat. TWMergeConfig goes with it. It was undocumented and the only built-in use was the prefix, so I would rather wait for someone to ask for it than carry the machinery on spec.

Measured:

Size +734 B gzip on a Vue playground build (1,194,063 vs 1,193,329). cn lands in one chunk, not duplicated. Bundled on their own with only the merge path used, cn is smaller minified (24.0 KB vs 27.4) and bigger gzipped (9.8 KB vs 8.5): tailwind-merge sheds about half its dist when tree shaken, cn's tables are data and shed a quarter, and they compress 2.4x against 3.2x.
Speed Within noise on all 13 engine benches, ±2% with outliers both ways. cn is 2.5x faster on cold merges, but the slot memo sits above the merger so those are rare in a render.

Two more paths measured since, both closed:

  • cn's headline shape is slower for us than what we already do. Its 30x row is cn(base, variant, cond && extra) with stable arguments, won by an argument-identity cache. On Button's real compiled parts: pre-joined string 33 ns with tailwind-merge and 19 ns with cn, stable variadic args 390 ns and 148 ns. Pre-joining with cx and handing over one string beats feeding that cache by 8x, so restructuring the engine for it would cost, not save.
  • Init is where cn wins. tailwind-merge is 1.45 ms to import plus 2.58 ms on first merge; cn is 3.20 ms to import plus 0.40 ms. About 0.4 ms once per process, against a 116 ms cold Button mount.

In a real render the merger is 0.66% of a cold Button toolbar (2 calls) and 0.09% of a cold 200x5 Table (9 calls). Warm renders call it zero times, since the slot memo sits above it.

So the ledger is 0.4 ms once, 14 ns per merge on a handful of merges, and 734 bytes. Keeping this open in case cn gets smaller after bundling, which is the whole of that 734 B. The deprecated-class and ui.tv renaming pieces landed separately in #7007.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@benjamincanac benjamincanac added the v5 #6918 label Sep 22, 2026
@socket-security

socket-security Bot commented Sep 22, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcn@​0.3.2861009993100

View full report

@pkg-pr-new

pkg-pr-new Bot commented Sep 22, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/ui@7006

commit: 96fee88

@codspeed

codspeed Bot commented Sep 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 35 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing feat/cn-merger (96fee88) with v5 (753fdd4)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

This branch was successfully deployed

1 active deployment
Preview – ui 96fee88f Deployed Sep 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v5 #6918

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant