Skip to content

class-recipe performance

  • 9×as fast as class-variance-authority
  • 13×as fast as tailwind-variants
  • 4×as fast as either, with tailwind-merge
  • 10×as fast as tailwind-variants, for slot recipes

Performance is the first goal of this library, and it is measured on every change to it. A recipe compiles its config once, when it is created, and caches the class name of each selection, so most calls are a lookup (see How it works).

Each benchmark calls the same recipe, written for each library, with six selections in turn, which cover default variants, compound variants, boolean variants, and class overrides. Every library returns the same classes. Without tailwind-merge, tailwind-variants runs from its lite entry point. With it, class-variance-authority calls twMerge on every result, as its documentation recommends.

Recipe benchmark
RecipeRelative to the fastest of its groupCalls/sCompared
Without tailwind-merge
class-recipe13.6M—
class-variance-authority1.49M9.1× slower
tailwind-variants1.03M13.2× slower
With tailwind-merge
class-recipe4.05M—
tailwind-variants1.03M3.9× slower
class-variance-authority953K4.3× slower

The same, for a slot recipe with six selections. class-variance-authority has no slots.

Slot recipe benchmark
Slot recipeRelative to the fastest of its groupCalls/sCompared
Without tailwind-merge
class-recipe8.55M—
tailwind-variants864K9.9× slower
With tailwind-merge
class-recipe5.21M—
tailwind-variants855K6.1× slower

cx is a drop-in replacement for clsx, and about as fast. On every input, it runs at least 100% as fast as clsx, and it is faster on 6 of the 6.

Calls per second on each input, in millions
Inputclass-recipeclsxclassnames
Strings16.916.315.8
An object14.914.814.0
An array16.014.813.7
Nested arrays9.79.18.4
Mixed values7.97.56.9
A component's classes16.114.215.1

The first call for a selection builds its class name by concatenating precomputed strings; every later call reads it from the cache.

Cache benchmark
CacheRelative to the fastest of its groupCalls/sCompared
Recipe, with cache13.3M—
Recipe, without cache7.87M1.7× slower
Slot recipe, with cache9.57M—
Slot recipe, without cache3.44M2.8× slower
  • The benchmarks first check that each call returns the expected classes, and that every library compared returns the same ones.
  • They import the package by its name, so they run the built bundle, not the sources. The test runner reads module exports through getters, so each benchmark holds the function under test in a local binding to keep that cost out of the measurement.
  • Each benchmark adds up the length of the class names it gets, so the measurement adds no work of its own, such as hashing strings.
  • The benchmarks assert that a recipe and a slot recipe are faster with their cache than without it.

Run them with pnpm bench, which builds the package first, or with pnpm measure, which also saves the results shown here. The comparisons with other libraries are in the *.compare.bench.ts files. The benchmarks are not part of CI, because shared runners are too noisy for timing assertions.