shadcn's cn: 30x Faster Drop-in Replacement for clsx + tailwind-merge
shadcn's new cn utility merges clsx and tailwind-merge into a single package, delivering up to 30x faster class-name merging via compile-time lookup tables and multi-layer caching, with a CLI for automatic migration, though it adds ~1.9KB gzipped and requires Tailwind CSS v4.
How the 30x Speedup Is Achieved
The article explains that the performance gain comes from moving work from runtime to compile time. tailwind-merge carries about 380 Tailwind conflict rule groups; at runtime it must split strings, look up rules, validate, and decide overrides. cn reads those rules at compile time and builds a flat lookup table. At runtime it scans the input string once, matches class names via a character tree stored in typed arrays, and turns conflict resolution into integer comparisons. When no classes need removal, it returns the original string directly.
Three caching layers further accelerate repeated calls:
Outermost layer remembers call arguments. React components often re-render with identical base styles, variants, and conditional classes. cn detects the same string objects and returns the previous result, even skipping the regular cache lookup by remembering call order.
Second layer caches full output strings, but only after a string appears twice, preventing one-time SSR strings from filling the cache.
Third layer caches individual class names that have been seen before.
The headline 30x figure comes from stable-argument calls like cn(base, variant, condition && extra). Official benchmarks show clsx + tailwind-merge at ~320 ns versus cn at ~10 ns.
30x Does Not Translate Directly to Page Speed
Full benchmark results are more nuanced:
Warmed common strings: 13 ns vs 7 ns (1.9x faster).
Cold calls with arbitrary values: ~3x faster.
SSR-style one-time strings: ~6.4x faster.
First call: from ~3.2 ms down to ~0.4 ms (7x faster).
All numbers remain in microseconds, so swapping the utility is unlikely to make a janky page suddenly smooth. Network requests, heavy computation, large DOM updates, and wasted renders typically dominate. The real value is eliminating the runtime interpretation cost of a pervasively called foundational function while keeping the API and output identical.
Compatibility is validated with 56,000+ differential test cases, 300,000+ syntax fuzz tests, 5,000+ custom-configuration cases (total 356,000), plus 144,265 real-world cn() calls collected from 58 open-source projects, yielding a geometric mean speedup of 37x. Tests run in isolated processes, warmed individually, best of 5 runs; benchmark scripts are published for independent verification.
Speed Comes with a Small Bundle-Size Cost
Default entry gzipped size: cn ~10.5 KB vs clsx + tailwind-merge ~8.6 KB (+1.9 KB). The increase stems from pre-compiled lookup tables. A cn build command can prune the tables based on classes actually used in the project; the more fixed the class set, the more effective this is.
A limitation: dynamically constructed class names (e.g., const className = "p-" + size) cannot be fully detected at build time, mirroring Tailwind's own content-scanning constraint. Developers must use safelist to preserve such classes.
Should You Migrate Now?
shadcn provides an official CLI migration command for Tailwind CSS v4 projects: npx shadcn@latest migrate cn The command rewrites imports of clsx, tailwind-merge, and early cnfast, detects common combined-call patterns, installs cn, and removes the old dependencies if they are no longer used elsewhere. A components.json file is not required.
The author treats this as a low-risk infrastructure update. New projects can adopt directly; existing projects should run tests first and check custom tailwind-merge configurations and rarely used APIs.
Tailwind CSS v3 projects should wait. Current cn targets Tailwind CSS v4; v3 projects are advised to stay on tailwind-merge v2. cn also does not yet support experimentalParseClassName, and the CLI requires Node.js 20 or higher.
Going forward, new Tailwind projects may no longer need to install two packages and manually write the same cn() boilerplate — that legacy snippet finally has a maintained successor.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Cultivation Path
Focused on sharing practical tech content about TypeScript, Vue 3, front-end architecture, and source code analysis.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
