How it all began

At many times, we check library code and say this is not a library I would expect best performance from.

When I started with shadcn/ui, I noticed many issues and bugs due to my limited knowledge with Radix UI or because of some fields really not working as expected. Coming from Fluent UI, where different sets of quirks existed espicaly for potals and dialog, I was surprised by how many dependencies existed in simple projects. I noticed a few noteworthy ones with tailwind-merge and clsx.

Today, I am sharing what I saw for tailwind-merge.

I quickly noticed and thought, what is the use of this? Then thought, why didn't I hit these cases (mostly because I used escape hatches with SCSS and design overrides with important (!))? But that also meant this opened my eyes to problems and things that this solves, especially for shadcn, as the base component list is much larger and with different variants with CVA.

But whenever I opened and used my app, I had a lingering feeling this is slow because string manipulation is being done inside of JS at runtime. But as I had also used MUI and Emotion, as well as styled-components, I was reminded this cannot be slower than those implementations as I carried forward.

Now that I was available for doing things, I checked in tailwind-merge code again. It's almost 1 year later, Tailwind v4 is out, and I started checking what might be things that can be optimized.

First things I noticed were creating an array may not be necessary. Also, there were multiple array creations within the codebase, which I checked and moved to a looped version using slice and indexOf with the help of AI (I was using DeepSeek v4 Flash(0731) for this project currently).

Next stop, I noticed we can skip computation for very short strings. If a string is less than 7 characters, we can skip this fully (p-4, p-5). This was the smallest thing I could think of, and if we do this, we save space in cache as well as overall compute, as in my projects in company I had lots of places which used cn("p-4",className) so each level called cn.

But I didn't trust the results. I was working in a customized Hermes agent as the first agent and OpenCode as my coding agent, and it consistently said there was more than a 20x improvement with just these parts combined on the bench it created itself.

I asked to publish it as a fork on GitHub probaly with some previous contex of me wanting to create PR It created a repo as well as 2 PRs, which once I noticed, I closed tose PR's as I my self didnt have much confidence.

On tailwind merge I wanted to solve one really big problem: I really don't like that custom Tailwind classes don't get addressed and merging doesn't work automatically. Also, I was looking into tools like Panda CSS and StyleX, which were other CSS implementations which said they moved CSS generation to build time with conflict resolution. So I decided to take the code of Tailwind CSS build-time Rust code and use that as a source of truth for merging CSS with scanned class names and fixing tailwind plugin requiring config , custom tailwind class auto detection.

Also, at the hot path, the repo was able to produce on non-cached items around a 30x improvement. It would create a custom function at build time. This time, I was still using DeepSeek v4 Flash and with OpenCode v2.

Then I left the project, There were two things one this still could lead to overwides when intersecting items would be there as tailwind custom classes may or may not be atomic also because I tried replacing the same in my website and it didn't add more optimization, as I had used in many places cno to reduce the number of slots in the cache.

typescript
// CN optimized needed name to be short and very simple
function cno(enabled: boolean, ...args: ClassValue[]) {
  if (!enabled) return clsx(args); //this overhead on code is not much 
  return twMerge(clsx(args)); 
}

Note: with cno implementation it pretty much made development more difficult with bad DX.

I also realized this was just too little improvement to be noticed and I should have directed my efforts to other places.

Recently, the shadcn team has created a newer version which solves most of the issues(twMerge speed) with move give and backward compatibility, so I am happy the right people were thinking about the same issue and they did it better than me and my model could.

Working next for SPFx dev and build where fewer people are optimizing. Check out rspfx: rspfx.mbsks.me.