Plate 91
tailwind-merge vs clsx: 8.6 KB vs 264 B
Aditya Challa4 min read
I bundled the two className helpers on this box: tailwind-merge 3.7.0 came out at 8,561 bytes gzip, while clsx 2.1.1 came out at 264 bytes. On 200k mixed-arg builds, clsx ran about 18.6M–20.0M ops/s versus about 4.4M–4.5M for twMerge, and only twMerge dropped conflicting Tailwind utilities.
Short answer: keep clsx (or clsx + a merge layer) when you only need conditional class strings. Reach for tailwind-merge when conflicting utilities like px-2 vs px-4 must resolve to one winner in the final class list. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 02:22 IST.
- Versions: tailwind-merge 3.7.0, clsx 2.1.1, Node 22.20.0, esbuild 0.28.2 browser ESM minify + gzip -9.
- Inputs: string + false/null conditionals + conflicting Tailwind tokens. Speed: 200,000 builds × 9-round median, two order-reversed runs. Scripts in /workspace/bench-tailwind-merge-clsx/.
- Findings: twMerge 27,407/8,561 B vs clsx 412/264 B. Medians: twMerge 44.881 / 45.925 ms (file1) and 45.011 / 45.379 ms (file2); clsx 10.740 / 10.635 ms and 10.688 / 9.997 ms (~4× more ops/s). Conflict: twMerge("px-2 px-4") → "px-4"; clsx keeps both. Disk ~1.0 MB vs ~8 KB.
- One surprise: twJoin still concatenates like clsx (px-2 px-4 stayed both classes); only twMerge applies the conflict group rules.
- Not tested: createTailwindMerge custom configs, RTL variants, Tailwind v4 token tables beyond the default, CSS Modules composition, or Rollup/webpack tree-shake matrices.
How much smaller is clsx?
| Build (esbuild browser minify) | Minified | gzip -9 |
|---|---|---|
| tailwind-merge | 27,407 B | 8,561 B |
| clsx | 412 B | 264 B |
About 32× smaller gzip for clsx. Pair this with the older class helper lab clsx vs classnames. Track client cost the same way as size-limit vs bundlesize. Tailwind-adjacent CSS tooling sits next to UnoCSS vs Tailwind and Lightning CSS vs PostCSS.
Does merge speed matter?
| 200k mixed builds (median of 9, 2 order-reversed runs) | tailwind-merge | clsx |
|---|---|---|
| Order twMerge then clsx (ms / ops/s, file 1) | 44.881 / ~4.46M | 10.740 / ~18.6M |
| Order clsx then twMerge (ms / ops/s, file 1) | 45.925 / ~4.35M | 10.635 / ~18.8M |
| File 2 repeats | 45.011 / 45.379 | 10.688 / 9.997 |
clsx stayed about 4× cheaper per call. That gap only matters in hot class rebuild loops; for typical React renders the conflict-resolution behavior usually wins the decision. Tiny string helpers in a nearby lane show up in picocolors vs chalk.
Do the APIs behave the same?
| Topic | tailwind-merge 3.7.0 | clsx 2.1.1 |
|---|---|---|
| Conditional false/null | drops them | drops them |
| "px-2 px-4" | "px-4" | "px-2 px-4" |
| Multi-arg conflict (bg-red + bg-blue) | last wins (bg-blue-500) | keeps both |
| Join without conflict rules | twJoin keeps both | n/a (clsx is join) |
They are not substitutes. clsx builds the string; twMerge also understands Tailwind conflict groups. Lint/format stacks that often ship with the same frontend decision sit next to Biome vs ESLint + Prettier. Asset pipelines nearby: SVGO vs SVGR.
Which one should you pick?
| Situation | My pick |
|---|---|
| Conditional classes only, no Tailwind conflicts | clsx |
| Smallest browser helper on this box | clsx |
| Tailwind utilities that override each other | tailwind-merge |
| Already on clsx, need conflict drops | clsx + twMerge together |
| Image/asset minify instead of class strings | see sharp vs jimp |
Bottom line: tailwind-merge 3.7.0 was 8,561 B gzip versus 264 B for clsx 2.1.1, clsx ran about 4× more ops/s, and only twMerge turned "px-2 px-4" into "px-4". Who should not drop twMerge: any Tailwind UI that relies on later classes winning conflicts. Who should not add it: projects that never emit conflicting utilities and care about every kilobyte.
How this was made: I installed both packages on the ShopperCove box, measured esbuild browser bundles, timed 200k class builds in two order-reversed runs, and compared conflict/conditional/twJoin behavior. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/dcastil/tailwind-merge
- https://github.com/lukeed/clsx
- https://www.npmjs.com/package/tailwind-merge
- https://www.npmjs.com/package/clsx
Related
- https://www.shoppercove.com/blog/clsx-vs-classnames
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/unocss-vs-tailwind
- https://www.shoppercove.com/blog/lightningcss-vs-postcss
- https://www.shoppercove.com/blog/picocolors-vs-chalk
- https://www.shoppercove.com/blog/biome-vs-eslint-prettier
- https://www.shoppercove.com/blog/svgo-vs-svgr
- https://www.shoppercove.com/blog/sharp-vs-jimp
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~02:22 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). tailwind-merge 3.7.0 vs clsx 2.1.1, esbuild 0.28.2 browser ESM minify + gzip -9. Size: 27,407/8,561 B vs 412/264 B. Speed 200k mixed builds x 9-round median, order-reversed: twMerge ~44.9-45.9 ms (~4.4M ops/s) vs clsx ~10.0-10.7 ms (~18.6-20.0M). Conflict: twMerge px-2 px-4 -> px-4; clsx keeps both. Disk ~1.0 MB vs ~8 KB. Not tested: createTailwindMerge, RTL, Tailwind v4 custom tables. No affiliate.