Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add performance test #253

Open
dcastil opened this issue Jun 15, 2023 Discussed in #252 · 6 comments
Open

Add performance test #253

dcastil opened this issue Jun 15, 2023 Discussed in #252 · 6 comments
Labels
context-v1 Related to tailwind-merge v1 feature request

Comments

@dcastil
Copy link
Owner

dcastil commented Jun 15, 2023

Discussed in #252

Originally posted by tordans June 15, 2023
It would be awesome to have the performance test that clsx uses run for twMerge and twJoin and have a page in the repo that shows the results.

I found existing references to performance, but in the end they do not really tell me how the libraries compare.

@dcastil
Copy link
Owner Author

dcastil commented Jun 15, 2023

Nice to haves would be:

  • Compare at least twMerge, twJoin and clsx
  • Test scenario should be somewhat realistic with some repetition and many unique calls like on app startup
  • Post performance benchmark on every new PR, ideally with diff to main branch as well
  • Link to benchmark from docs

@larry-cedar
Copy link

larry-cedar commented Jun 22, 2023

twMerge is incredibly useful and it perfectly solved tailwind utility classes conflict issues. however my big concern is performance.
I do a simple test with below code. it took over 2ms which is huge. as a comparison, using classNames is almost 0.

  const cn1 =
    "flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75";
  const cn2 =
    "flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75";
  const cn3 = "flex items-center w-full justify-between tracking-0_2 hover:bg-gray-500 cursor-pointer px-16 px-24";  
let start = performance.now();
  twMerge(
    cn1,
    cn2,
    cn3,
  );
  let end = performance.now();
  console.log(`Execution time: ${end - start} ms`);
//classNames test
  classNames(
    cn1,
    cn2,
    cn3,
  );

for a big page that can easily have thousands of DOM nodes, that will be slow. due to the performance concern, we can not recommend twMerge in our project.

@dcastil
Copy link
Owner Author

dcastil commented Jun 23, 2023

Hey @larry-cedar! 👋

tailwind-merge computes a large data structure on the first call to twMerge which it caches for future calls which makes the first call probably 1000x slower than subsequent calls (you can read about it here). Therefore comparing a single twMerge call with a single classNames call doesn't really make sense. If you do a simple comparison like that, I'd suggest to do more calls, e.g. 1000.

  const cn1 =
    "flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75";
  const cn2 =
    "flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75";
  const cn3 = "flex items-center w-full justify-between tracking-0_2 hover:bg-gray-500 cursor-pointer px-16 px-24";

  let twMergeStart = performance.now();
  for (let i = 0; i < 1000; i++) {
    twMerge(
      cn1,
      cn2,
      cn3,
    );
  }
  let twMergeEnd = performance.now();
  console.log(`twMerge execution time: ${twMergeEnd - twMergeStart} ms`);

  let classNamesStart = performance.now();
  for (let i = 0; i < 1000; i++) {
    classNames(
      cn1,
      cn2,
      cn3,
    );
  }
  let classNamesEnd = performance.now();
  console.log(`classNames execution time: ${classNamesEnd - classNamesStart} ms`);

But that comparison is still not very realistic because twMerge calls 2-1000 will all be cache hits. You get a better comparison when you call twMerge often with different inputs.

I wrote about a realistic test case here: #243 (comment)

Results of reloading the app running this code in development mode with Vite on a MacBook Pro 16" with a M1 Pro chip (from 2021) in the Chrome browser with devtools open:

349 calls to twMerge with a total of 5.1 ms execution time.

@larry-cedar
Copy link

Hey @dcastil thanks for your reply. I see twMerge cache the computation. but still the initial rendering will be very slow. I am trying to solve this problem, but TBH, i don't have better idea on how to make the first call faster.

@dcastil
Copy link
Owner Author

dcastil commented Jun 24, 2023

There's no way of making the first call faster at the moment unfortunately. You can check out some alternatives to tailwind-merge here: https://github.com/dcastil/tailwind-merge/blob/v1.13.2/docs/when-and-how-to-use-it.md#alternatives

@dcastil
Copy link
Owner Author

dcastil commented Jul 9, 2023

Research:

What I want

  • In every PR, post a comment with benchmark
  • Benchmark comment should include perf results of PR branch, base branch and their diff
  • Comment should update when further commits are pushed to PR
  • Ideally ability to rerun benchmark on PR because of fluctuation in results over time

Github Action

Data

  • Code to extract twMerge calls from app:

    export const twMergeInternal = extendTailwindMerge({ /* … */ })
    
    const twMergeCalls: any[] = []
    
    export function twMerge() {
        twMergeCalls.push(Array.from(arguments))
    
        return twMergeInternal.apply(null, arguments as any)
    }
    
    window.createPerfDataJson = () => {
        console.log(
            JSON.stringify(twMergeCalls, (key, value) => {
                if (value === undefined) {
                    return null
                }
    
                return value
            }),
        )
    }
  • Maybe I should also extract twJoin calls to perf test those.

@dcastil dcastil added the context-v1 Related to tailwind-merge v1 label Oct 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
context-v1 Related to tailwind-merge v1 feature request
Projects
None yet
Development

No branches or pull requests

2 participants