DEV Community

Remdore
Remdore

Posted on AI-assisted

ESLint took 4.4s to lint Vue's core. oxlint took 0.24s. Then I turned on the type-aware rules.

ESLint took 4.4 seconds to lint 445 TypeScript files. oxlint took 0.24. Turn on the type-aware rules that most projects actually want and ESLint climbs to 12 seconds, while oxlint, doing the type-aware pass too, finishes in under one. I went in expecting to write that oxlint was fast but limited. The limited part did not survive the test.

The setup

oxlint is the JavaScript and TypeScript linter from VoidZero, the company Evan You started after Vue and Vite, written in Rust and pitched as a much faster ESLint. I wanted a real number on a real codebase, not a toy, so I cloned the Vue core repository, 445 TypeScript files and about 150,000 lines, and linted the same tree four ways in a clean Node 20 container.

Two tools, two modes each. ESLint 9 with typescript-eslint, once with the plain recommended rules and once with the type-aware recommendedTypeChecked set that needs the TypeScript compiler behind it. Then oxlint, once with its defaults and once with --type-aware. Every number below is a wall-clock median of repeated runs on the same files.

Linter Mode Time
ESLint recommended (syntactic) 4.4s
ESLint recommendedTypeChecked (type-aware) 12s
oxlint default (syntactic) 0.24s
oxlint --type-aware 0.9s

The syntactic number is 18x, and that undersells it

On the plain recommended rules, oxlint linted the tree in 0.24 seconds against ESLint's 4.4, about eighteen times faster. But even that includes Node starting up and npx resolving the binary. oxlint prints its own engine time at the end of a run, and for this codebase it was 75 milliseconds. The actual linting of 150,000 lines took less than a tenth of a second; the rest was process startup.

That is the difference between a linter you run in a pre-commit hook and one you run in CI and hope finishes. At 75 milliseconds you can lint on every keystroke. At 4.4 seconds you lint when you remember to, or you wait for the CI job to tell you.

Type-aware is where I was wrong

Here is the part I got wrong, and it is the more interesting result.

The rules developers actually reach for are the type-aware ones. no-floating-promises, no-misused-promises, await-thenable: the checks that catch the bugs that matter, and every one of them needs the type checker to know what a value actually is. That is also why they are slow. ESLint's type-aware pass on this codebase took 12 seconds, nearly triple its syntactic run, because it has to build a TypeScript program before it can lint a line.

I had written, in my head, the tidy conclusion: oxlint is fast because it skips this, it does the cheap syntactic rules and leaves the expensive type-aware ones to ESLint. A fast first pass, not a replacement.

Then I checked, and oxlint has a --type-aware flag. It runs the type-aware rules through a companion called tsgolint, and on the same codebase it finished in 0.9 seconds. Type-aware. Under a second. That is roughly thirteen times faster than ESLint doing the same class of work, and it is still five times faster than ESLint's cheap syntactic run.

So the story is not "oxlint is fast because it does less". oxlint doing the expensive, type-aware analysis beat ESLint doing the cheap, syntactic one, by a factor of five.

The honest caveats, because there are several

This is where I have to be careful, because a lint-time comparison is easy to make unfair.

The rule sets are not identical. ESLint's recommended set and oxlint's default set overlap heavily but are not the same list, so I am measuring the time to lint the same files to a comparable standard, not the time to run byte-for-byte identical checks. oxlint reported 96 rules on the syntactic run and 111 with type-awareness on; typescript-eslint's sets are their own count. If you need one specific ESLint rule or a custom plugin that only exists for ESLint, oxlint may simply not have it, and no speed number rescues a missing rule.

The type-aware mode is newer and needs help. It is behind a flag, it pulls in the separate oxlint-tsgolint package, and oxlint itself calls the type-checking part experimental. When I first ran --type-aware without that package it failed cleanly and told me what to install, which is the right behaviour, but it is a step, not a default. The 0.9-second number is real; it is just not what you get from a bare oxlint invocation.

And the type checker underneath is a reimplementation, not tsc. tsgolint is a Go-based TypeScript type checker, and the whole reason it is fast is that it is not the official compiler. For linting that is a reasonable trade, but it is a different implementation of the type rules, and "different implementation" is exactly the phrase that should make you test it against your own code before trusting it in CI.

Where a football site feels this

A TypeScript frontend like ExtraTime runs the linter constantly: on save in the editor, on every commit through a pre-commit hook, on every pull request in CI. At ESLint's 4 to 12 seconds, the pre-commit hook is the thing everyone eventually adds --no-verify to get around, and the CI lint job is a minute of a runner's life on every push. At oxlint's sub-second, the hook stops being an interruption and the CI line stops being something you notice. It is the same code being checked; the difference is whether checking it is fast enough that nobody tries to skip it.

What I got wrong on the way

I nearly published the wrong limitation. My draft, before I ran the last test, said oxlint's ceiling was that it does not do type-aware linting, that it is a fast syntactic pass and you still need ESLint for the rules that need types. That is the received wisdom about fast linters and I repeated it without checking.

It took one --help to find the --type-aware flag and one more run to get 0.9 seconds out of it. The lesson is the one that keeps recurring: the tidy limitation that makes your narrative balanced is exactly the claim you have not tested. I had a clean story about a fast-but-limited tool, and the limit was not there. The real caveats, the ones above, are messier and less quotable, which is usually how you can tell they are the real ones.

Run it yourself

git clone --depth 1 https://github.com/vuejs/core
cd core && npm i -D eslint typescript-eslint typescript oxlint oxlint-tsgolint

# ESLint, syntactic then type-aware
time npx eslint "packages/**/*.ts"
time npx eslint -c eslint.typed.mjs "packages/**/*.ts"

# oxlint, syntactic then type-aware
time npx oxlint packages
time npx oxlint --type-aware packages
Enter fullscreen mode Exit fullscreen mode

Watch the number oxlint prints at the end of its own run, the "finished in Nms" line. That is the linting; everything else your time shows is Node waking up.

What to do about it

Add oxlint as the fast gate and keep the slow one where it earns its place. Run oxlint in the pre-commit hook and the first CI step, because at sub-second cost there is no reason not to, and it will catch the great majority of issues before anything slower starts. Keep ESLint for the specific rules or plugins oxlint does not have yet, and run it once in CI rather than on every save. The point is not to pick a winner. It is that the fast tool is now fast enough to run everywhere, and the only question left is which handful of checks still justify the slow one.

Top comments (0)