Skip to content

perf: cache canonical classes and fix misaligned canonicalization - #20

Merged
MaisonnatM merged 3 commits into
mainfrom
perf/speed-up-rule
Oct 20, 2026
Merged

MaisonnatM merged 3 commits into
mainfrom
perf/speed-up-rule

Conversation

@MaisonnatM

@MaisonnatM MaisonnatM commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Why

--fix could rewrite valid classes to the wrong ones. In a 400-file benchmark, the old code reported 337 bogus warnings like Class 'gap-2' should be 'bg-blue-500'. The rule was also slow: about 2.1 s on that project, against 0.37 s for ESLint with the rule off.

What changed

  • Misaligned canonicalization (bug). Tailwind's canonicalizeCandidates removes duplicates from its output, so the result shifted against the input indexes whenever two classes canonicalized to the same value. The worker now canonicalizes one class at a time. A new integration case runs the real worker on gap-[8px] gap-2 p-2; it fails before this change and passes after.
  • Stale design system (bug). The worker cached the design system by directory and never reloaded it. It now reloads when any file Tailwind read to build it changes.
  • Cache. Canonical forms are cached in memory and in node_modules/.cache/eslint-plugin-tailwind-canonical-classes/ under the ESLint cwd, or in the system temp directory when there is no node_modules. The worker reports every file Tailwind read to load the CSS, via compile's onDependency. One mtime and size check over those files, run once per linted file, invalidates the in-memory, disk and worker caches together. That covers imported themes and npm upgrades, including in long-lived editors. Results are merged into the disk file on each miss, so concurrent linters add to the cache instead of overwriting each other. TAILWIND_CANONICAL_CLASSES_CACHE_DIR overrides the location. The worker only starts on a cache miss.
  • Less work per node. The CSS file is checked once per linted file instead of once per class attribute. A CallExpression only walks its ancestors when its callee is one of the configured utility functions (cn, clsx, ...). The CSS-not-found branch in the pipeline became unreachable, so it is removed.

Measurement

npm run bench lints 400 JSX files with 40 elements each. Medians of 7 alternating runs on a loaded machine:

  • Before: 2094 ms
  • After, warm cache: 526 ms (the rule-off control measured 407 ms in the same runs)
  • After, cold cache: 1658 ms. About 920 ms of that is Tailwind building its canonicalization index on its first call.

Warnings match before and after, minus the misaligned ones.

Checks

  • Thermo-nuclear code quality review

Tailwind dedupes canonicalizeCandidates output, so index-aligned results
shifted and --fix rewrote valid classes. Canonicalize per candidate.

Cache canonical forms in memory and on disk keyed by Tailwind version,
rootFontSize and the local CSS import graph; spawn the worker only on a
cache miss. Warm 400-file lint: 2094 ms -> ~500 ms.
Address review of the disk cache:
- untrack bench/.out and restore the test-env/ ignore line
- the worker reports the files Tailwind read (compile onDependency); one
  stat stamp over them invalidates memory, disk and worker caches together
- write-through merge on miss replaces the lossy exit-time flush
- resolve the cache dir from ESLint cwd; fall back to tmpdir
- key on plugin version and package versions of imported files
@MaisonnatM
MaisonnatM marked this pull request as ready for review October 20, 2026 07:41
@MaisonnatM
MaisonnatM merged commit a6ec391 into main Oct 20, 2026
6 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.4.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

MaisonnatM added a commit that referenced this pull request Oct 20, 2026
* perf: append new canonical classes to the cache instead of rewriting it

Each miss rewrote the whole table, so a cold run over 24k unique classes took 143 s instead of 24 s.

* fix: key canonical cache on rem and engine version

- Fold rootFontSize and the @tailwindcss/node version into the table key; drop per-package version walking.
- Write the dependency manifest only when the worker reports a new file list.
- Move resolveCacheDir next to the cache and key the in-memory cache on the cache dir.
- Test that a warm disk cache never starts Tailwind.

* refactor: one cache write path, atomic manifest, engine keyed as a file
github-actions Bot pushed a commit that referenced this pull request Oct 20, 2026
## [1.4.4](v1.4.3...v1.4.4) (2026-10-20)

### Bug Fixes

* speed up cold canonical cache and apply [#20](#20) review follow-ups ([#25](#25)) ([7954b59](7954b59))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant