Performance of `enforce-consistent-important-position`
👋 @schoero!
I just tried enforce-consistent-important-position in v3.6.0. It moved all existing !s to the end of the class names as expected which was great. However, the rule appeared to be slower than I expected. Here is what I saw after disabling all ESLint rules except those from better-tailwindcss:
TIMING=20 pnpm lint:eslint
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/enforce-consistent-important-position | 5688.334 | 38.6%
better-tailwindcss/no-unregistered-classes | 4154.601 | 28.2%
better-tailwindcss/enforce-consistent-class-order | 1935.486 | 13.1%
better-tailwindcss/enforce-shorthand-classes | 1120.175 | 7.6%
better-tailwindcss/no-conflicting-classes | 941.096 | 6.4%
better-tailwindcss/enforce-consistent-variable-syntax | 300.088 | 2.0%
better-tailwindcss/no-unnecessary-whitespace | 214.585 | 1.5%
better-tailwindcss/no-duplicate-classes | 208.988 | 1.4%
better-tailwindcss/no-restricted-classes | 188.890 | 1.3%
It’s a medium-sized codebase and if I enable all rules, enforce-consistent-important-position takes 10-15% of the total time. I’d expect it to cost about the same as enforce-consistent-variable-syntax given that the rule must be syntactic. I don’t have many occurrences of ! so there may be an opportunity for some early exit based on the character absence.
I haven’t had a chance to investigate what is happening, just want to flag a potential issue first. I might have a bit of time on a weekend but it couldbe be something completely trivial for you as the author of https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/167.
Cheers! Loving the plugin!
Oh wow that seems really bad. The only thing that is really a bottleneck in this plugin is loading the tailwind context. But that is also loaded in the no-duplicate-classes rule which is much faster.
I also can't reproduce it with my repositories. These are the results for my largest repository:
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/enforce-consistent-line-wrapping | 514.066 | 4.7%
better-tailwindcss/no-unregistered-classes | 468.017 | 4.3%
better-tailwindcss/enforce-consistent-important-position | 265.837 | 2.4%
better-tailwindcss/enforce-consistent-class-order | 208.198 | 1.9%
Maybe it is something with your environment? 🤔
Can you provide some information about it? Specifically your:
- Operating System
- Node version
- ESLint version
- Tailwindcss version
Maybe it is something with your environment? 🤔
Sure!
Operating System: macOS 15.5 Node version: 20.19.3 ESLint version: 9.31.0 Tailwindcss version: 4.1.11
I just ran TIMING=20 pnpm lint:eslint again and got this:
With enforce-consistent-important-position on
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/enforce-consistent-important-position | 5322.685 | 38.1%
better-tailwindcss/no-unregistered-classes | 3976.893 | 28.4%
better-tailwindcss/enforce-consistent-class-order | 1822.769 | 13.0%
better-tailwindcss/enforce-shorthand-classes | 1081.038 | 7.7%
better-tailwindcss/no-conflicting-classes | 917.571 | 6.6%
better-tailwindcss/enforce-consistent-variable-syntax | 264.844 | 1.9%
better-tailwindcss/no-unnecessary-whitespace | 213.760 | 1.5%
better-tailwindcss/no-duplicate-classes | 193.885 | 1.4%
better-tailwindcss/no-restricted-classes | 187.146 | 1.3%
With enforce-consistent-important-position off
Rule | Time (ms) | Relative
:-----------------------------------------------------|----------:|--------:
better-tailwindcss/no-unregistered-classes | 4479.334 | 48.1%
better-tailwindcss/enforce-consistent-class-order | 1831.806 | 19.7%
better-tailwindcss/enforce-shorthand-classes | 1137.873 | 12.2%
better-tailwindcss/no-conflicting-classes | 916.736 | 9.8%
better-tailwindcss/enforce-consistent-variable-syntax | 269.572 | 2.9%
better-tailwindcss/no-duplicate-classes | 238.913 | 2.6%
better-tailwindcss/no-restricted-classes | 225.252 | 2.4%
better-tailwindcss/no-unnecessary-whitespace | 213.187 | 2.3%
As you can see, the total cost in the second case is significantly lower. no-unregistered-classes got slower by 0.5s, possibly because of what config loading is attributed to. So it looks like the actual cost of enforce-consistent-important-position is quite high.
If I switch to Node 22.17.0 (latest LTS), the situation does not change much:
With enforce-consistent-important-position on
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/enforce-consistent-important-position | 3682.656 | 32.5%
better-tailwindcss/no-unregistered-classes | 3099.231 | 27.3%
better-tailwindcss/enforce-consistent-class-order | 1822.445 | 16.1%
better-tailwindcss/enforce-shorthand-classes | 1096.525 | 9.7%
better-tailwindcss/no-conflicting-classes | 907.554 | 8.0%
better-tailwindcss/enforce-consistent-variable-syntax | 219.489 | 1.9%
better-tailwindcss/no-restricted-classes | 174.684 | 1.5%
better-tailwindcss/no-unnecessary-whitespace | 173.914 | 1.5%
better-tailwindcss/no-duplicate-classes | 171.309 | 1.5%
With enforce-consistent-important-position off
Rule | Time (ms) | Relative
:-----------------------------------------------------|----------:|--------:
better-tailwindcss/no-unregistered-classes | 3629.544 | 43.0%
better-tailwindcss/enforce-consistent-class-order | 1866.958 | 22.1%
better-tailwindcss/enforce-shorthand-classes | 1207.709 | 14.3%
better-tailwindcss/no-conflicting-classes | 922.569 | 10.9%
better-tailwindcss/enforce-consistent-variable-syntax | 231.694 | 2.7%
better-tailwindcss/no-duplicate-classes | 216.464 | 2.6%
better-tailwindcss/no-restricted-classes | 195.566 | 2.3%
better-tailwindcss/no-unnecessary-whitespace | 162.561 | 1.9%
The project is about 100K lines of code (ts + tsx). I’ll try to find some time later to investigate this properly.
Just ran ESLint in CI with TIMING=20 again, seeing this:
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/no-unregistered-classes | 32890.086 | 29.3%
better-tailwindcss/enforce-consistent-important-position | 19197.023 | 17.1%
better-tailwindcss/enforce-shorthand-classes | 15266.860 | 13.6%
better-tailwindcss/enforce-consistent-variable-syntax | 12926.864 | 11.5%
better-tailwindcss/no-deprecated-classes | 12866.063 | 11.5%
better-tailwindcss/no-conflicting-classes | 8926.961 | 8.0%
better-tailwindcss/enforce-consistent-class-order | 7856.204 | 7.0%
better-tailwindcss/no-restricted-classes | 785.600 | 0.7%
better-tailwindcss/no-duplicate-classes | 702.925 | 0.6%
better-tailwindcss/no-unnecessary-whitespace | 644.064 | 0.6%
Execution time for better-tailwindcss rules alone is approaching two minutes. Total ESLint's time in GitHub CI is around five minutes, including heavy tsc-dependent typescript-eslint rules. The codebase contains about 1K TSX files.
I haven't had time to get deeper into this, just sharing overall stats.
I wonder if there is some way of caching results between files for tokens. For example, if we have p-0 mentioned in hundreds of files, we can check if the class is registered once and put the result into a global lookup. The presence of the same class in other files won't add more calls to regexps etc. Same for important position, shorthands etc. If !p-0 has already been checked and flagged as incorrect (→ p-0!), we can re-use the result from some global lookup and avoid expensive re-checks.
That's just a guess about the current rule slowness after a quick peek into the plugin's code. Maybe this kind of caching is already done and I have just missed it.
I'm running into similar issues. It's particularly slow for me on CI.
Local Development
- Operating System: macOS 15.7
- Node version: 24.8.0
- ESLint version: 9.36.0
- Tailwindcss version: 4.1.13
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/no-unregistered-classes | 4559.672 | 12.9%
better-tailwindcss/enforce-consistent-line-wrapping | 3997.531 | 11.3%
better-tailwindcss/enforce-consistent-important-position | 2864.522 | 8.1%
...
CI
- Operating System: RHEL 9.6
- Node version: 24.8.0
- ESLint version: 9.36.0
- Tailwindcss version: 4.1.13
Rule | Time (ms) | Relative
:--------------------------------------------------------|-----------:|--------:
better-tailwindcss/no-unregistered-classes | 627986.515 | 41.7%
better-tailwindcss/enforce-consistent-line-wrapping | 607354.597 | 40.4%
...
I have created a v4 branch where I have restructured the plugin a bit. Mainly because I wanted to decouple it from ESLint to better support oxlint once they add support for JavaScript plugins. In this refactoring, I have also optimized the way the plugin is loaded and how it caches some things.
This might improve the performance a bit, but I don't expect a significant change, because most things are already cached.
I spent the last couple of evenings trying to find any other bottlenecks in this plugin. Unfortunately, I could not find any.
The basic problem of this plugin is that lots of rules require the tailwind context to work. Creating the tailwind context is asynchronous since tailwind 4. ESLint is not asynchronous. Because of this, the plugin uses synckit to be able to call the asynchronous tailwind context creation function synchronously.
Synckit runs the asynchronous code in worker threads which then can be executed synchronously. The creation of the tailwind context itself takes only a few ms but the creation of the worker threads sometimes take a long time. On my development MacBook Air M2, this ranges from 100ms to around 300ms. I found that in CI pipelines, this can take much longer, up to a few seconds. I think this is just when the CI runners are under a heavy load.
However, the worker threads and the tailwind context are also cached. So this additional delay only happens once when the rules are initialized.
I found that the subsequent calls of all rule are generally < 1ms. The synckit worker calls are mostly around 100µs. So I don't think there is much room for improvement here.
Maybe you can try with the latest v4 beta and see if that improves it:
npm install eslint-plugin-better-tailwindcss@beta
Note that it already has some breaking changes.
I have just tried v4.0.0-beta.2 and wow - it’s quite a big difference!
v3 = 112s
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/no-unregistered-classes | 33270.112 | 29.6%
better-tailwindcss/enforce-consistent-important-position | 19254.103 | 17.1%
better-tailwindcss/enforce-shorthand-classes | 15200.548 | 13.5%
better-tailwindcss/enforce-consistent-variable-syntax | 12902.688 | 11.5%
better-tailwindcss/no-deprecated-classes | 12779.206 | 11.4%
better-tailwindcss/no-conflicting-classes | 8898.058 | 7.9%
better-tailwindcss/enforce-consistent-class-order | 7774.521 | 6.9%
better-tailwindcss/no-restricted-classes | 794.258 | 0.7%
better-tailwindcss/no-duplicate-classes | 709.400 | 0.6%
better-tailwindcss/no-unnecessary-whitespace | 692.140 | 0.6%
v4 = 30s
Rule | Time (ms) | Relative
:--------------------------------------------------------|----------:|--------:
better-tailwindcss/no-unknown-classes | 12165.263 | 41.3%
better-tailwindcss/enforce-consistent-class-order | 4179.552 | 14.2%
better-tailwindcss/enforce-shorthand-classes | 3213.230 | 10.9%
better-tailwindcss/no-conflicting-classes | 2228.822 | 7.6%
better-tailwindcss/enforce-consistent-important-position | 1828.565 | 6.2%
better-tailwindcss/enforce-consistent-variable-syntax | 1780.102 | 6.0%
better-tailwindcss/no-deprecated-classes | 1740.293 | 5.9%
better-tailwindcss/no-unnecessary-whitespace | 885.011 | 3.0%
better-tailwindcss/no-duplicate-classes | 737.209 | 2.5%
better-tailwindcss/no-restricted-classes | 667.681 | 2.3%
Big thanks for your work on performance and for including a link to breaking changes in the comment above. I really look forward to 4.0.0! 👏
PS: We're not using a couple of rules like enforce-consistent-line-wrapping so the test is not comprehensive.