DEV Community

Cover image for We let Oxlint loose on a real React codebase. The speed wasn't the surprise.
The Release Desk
The Release Desk

Posted on AI-assisted

We let Oxlint loose on a real React codebase. The speed wasn't the surprise.

In 1787, Thomas Rowlandson drew an ox loose on Westminster Bridge. Hats flying, people climbing the railings, total panic.

That's roughly how it feels to drop a Rust linter into a five-year-old JavaScript project.

Oxlint and Oxfmt are the linter and formatter from the Oxc project. Between them they're downloaded about 26 million times a week, and the docs make bold claims: Oxlint is "50 to 100 times faster than ESLint", and Oxfmt is "approximately 30x faster than Prettier."

Bold claims deserve a stopwatch. So we picked a real, popular React codebase and timed both setups on the same laptop.

But the stopwatch turned out to be the boring part.

The test

We used Excalidraw, the open-source whiteboard app. It's a big React + TypeScript codebase (710 JS/TS files) with the setup a lot of teams still have: ESLint 7 using Create React App's rules, with Prettier 2.6 running inside ESLint as a plugin.

The machine was an ordinary laptop: Ryzen 5 5600H, 12 threads, 15 GB RAM. Each command got one warm-up run and three timed runs, and we report the median (the middle one).

Bar chart of lint and format times on Excalidraw: ESLint with the Prettier plugin 60.2 s, ESLint alone 50.7 s, Prettier 2.6 check 15.3 s, Oxlint with ported rules 1.5 s, Oxfmt check 0.41 s.

What we ran Time
ESLint + Prettier plugin (how Excalidraw ships it) 60.2 s
ESLint with the Prettier rule switched off 50.7 s
Prettier 2.6 --check on its own 15.3 s
Oxlint, with Excalidraw's rules ported over 1.5 s
Oxfmt --check 0.41 s

So the full check went from one minute to about two seconds. That's roughly 30x faster: real, but well short of the "50 to 100x" headline. The gap makes sense, because every plugin you switch on costs time. Oxlint with zero config took 0.46 s; with React, import and accessibility rules on, it took 1.3–1.5 s.

One bonus finding before we move on: the Prettier plugin alone cost about 10 seconds per run. If you run Prettier inside ESLint, you can split them apart today without switching any tools.

The part nobody puts in the benchmark

ESLint reported 0 warnings. Oxlint, with the same rules ported over, reported 435 warnings and 1 error.

At first glance that looks like Oxlint found 435 bugs that ESLint missed. It didn't. When we went through them:

  • 145 were unused variables and parameters. Oxlint's version of the rule is stricter by default.
  • 48 of the 50 "this number loses precision" warnings came from one file: a table of 40-digit math constants, written that way on purpose.
  • The 1 error was a target="_blank" link that already had rel="noopener", built from a template string. Excalidraw had already told ESLint to skip that check on that line.

Then the useful ones. Oxlint's React rules flagged 22 places that call setState directly inside an effect (the newer React guidance says to calculate the value during render instead) and 10 async Promise executors. Those are worth a look. ESLint 7's old rules simply don't know about them.

So here's the honest version:

  • First 2 seconds: "wow."
  • Next 20 minutes: "wait, which of these are real?"
  • Next week: "who decides which rules we actually want?"

The speed is free. The rule review is the real work.

What didn't move over

Three of Excalidraw's own rules have no built-in Oxlint version:

  1. no-restricted-syntax: Excalidraw uses it to stop plain text in JSX ("use t(...) instead"), which keeps the app translatable. That's a real guard you'd lose.
  2. import/order: Oxfmt can sort imports instead, but that's a formatting change, not a lint error.
  3. dot-notation: minor.

Oxlint's answer is JS plugins: you load the original ESLint plugin through Oxlint. That feature is still marked alpha. The other option is the official "run both" setup: oxlint && eslint, with eslint-plugin-oxlint turning off the rules Oxlint already covers.

One more trap: the automatic migration tool (@oxlint/migrate) only reads ESLint v9/v10 "flat" configs. Excalidraw still uses the old .eslintrc.json, so we ported its config by hand. If your project is on ESLint 8 or older, budget time for that too.

The formatter is the easy win

Oxfmt has its own migration command, and it worked on the first try:

npx oxfmt --migrate=prettier
Enter fullscreen mode Exit fullscreen mode

It even caught a trap for us. Oxfmt's default line width is 100, while Prettier's is 80, and the tool kept Excalidraw at 80.

Oxfmt wanted to reformat 102 files (+1,163 / −1,092 lines). That sounds scary until you compare it with Prettier 3, which wants to change 126 files in the same repo. Every one of Oxfmt's 102 files was also on Prettier 3's list. So the churn comes from leaving Prettier 2's old style, not from Oxfmt being strange. You'd pay it either way.

Bonus: Prettier 2.6 can't even parse one of Excalidraw's TypeScript files (newer generic syntax). The team had added a "skip Prettier here" comment above it. Oxfmt read it fine.

What to do on Monday

  1. Got Prettier inside ESLint? Take it out (eslint-config-prettier stays, eslint-plugin-prettier goes). That's ~10 seconds saved per run, with no new tools.
  2. Try Oxfmt in a branch: npx oxfmt --migrate=prettier, then npx oxfmt. Commit the reformat as one separate commit and add its hash to .git-blame-ignore-revs, so git blame stays useful.
  3. Run Oxlint next to ESLint, not instead of it: npx oxlint first, read the warnings, and decide rule by rule. Replace ESLint only when nothing you rely on is left behind.

You'll be in good company. Bluesky's app already runs Oxlint as its lint script, and the Oxc docs list Sentry, Kibana, Electron and PostHog as users.

Desk verdict: Switch the formatter now; it's a near drop-in. Bring in the linter next to ESLint, and only remove ESLint after you've reviewed the rules, because the time goes into that review, not into waiting.

Top comments (0)