Oxlint vs ESLint: Rust-Powered Linting Performance 2026
TL;DR
Oxlint is 50-100x faster than ESLint and ready for production — but as a complement, not a replacement. It runs first as a fast pass catching common errors (500+ rules), then ESLint handles TypeScript-aware rules and plugins that Oxlint doesn't support yet. The Vercel team runs both in CI: oxlint in < 1 second, ESLint in parallel only for the rules oxlint misses. For solo TypeScript projects, oxlint + ESLint is the performance-optimal stack in 2026.
Key Takeaways
- Oxlint: ~400K downloads/week, written in Rust (via oxc), 500+ rules, ~1s on large codebases
- ESLint: 42M downloads/week, ecosystem standard, TypeScript-aware rules, full plugin system
- Speed: Oxlint is 50-100x faster than ESLint on the same rules
- Coverage: Oxlint covers ~50-60% of what most projects need from ESLint
- Strategy: Run oxlint as fast pre-check, ESLint for type-aware rules only
- No plugins: Oxlint doesn't support custom plugins (yet) — ecosystem rules (Tailwind, a11y) still need ESLint
Why Linting Is Slow (And Why Rust Fixes It)
ESLint was designed in 2013 when JavaScript was the only option for JavaScript tooling. Its architecture reflects that era: plugins are JavaScript modules, rules run in JavaScript, the AST is built in JavaScript, and each file is processed mostly sequentially.
For small projects, none of this matters. For a 1,200-file Next.js codebase with TypeScript-aware rules, ESLint can take 30-60 seconds on a cold run. That's long enough that developers skip running it locally and only discover lint errors in CI.
Why Oxlint is faster:
-
Rust parallel processing. Oxlint processes files across all CPU cores simultaneously. A 32-core CI machine gets near-linear speedup on file count. ESLint is single-threaded by default.
-
No plugin overhead. ESLint plugins are npm packages loaded and parsed at startup. Oxlint's rules are compiled into the binary. There's no JavaScript module loading, no plugin resolution, and no V8 startup cost.
-
Single-pass analysis. Oxlint parses each file once and runs all applicable rules in that single pass. ESLint's architecture has each rule visiting AST nodes separately.
-
No type-checking by default. ESLint's TypeScript-aware rules (
@typescript-eslintwithparserOptions.project) require TypeScript to build the full type graph. This is expensive. Oxlint runs only syntax-level rules — fast but less powerful.
Performance Benchmark
On a realistic Next.js project with 1,200 TypeScript files and 85,000 lines:
ESLint (full config: typescript-eslint, react, import, tailwindcss):
Cold run: ~45s
Warm run: ~38s
ESLint (minimal — type-aware rules only, no react/import/tailwind):
Cold run: ~12s
Warm run: ~8s
Oxlint (equivalent non-type-aware rules):
Cold run: 0.4s
Warm run: 0.4s (Rust binary, no JS startup cost)
Optimal combined stack:
oxlint (first pass): 0.4s catches ~60% of errors instantly
eslint-minimal (second): 8s type-aware rules in parallel with oxlint
Total effective wall time: 8s vs 38s for ESLint-only
The wall-time improvement in CI depends on how you schedule the jobs. If you run oxlint and ESLint in parallel (fail fast on oxlint), developers get error feedback in under a second while ESLint finishes in the background.
Rule Coverage: What Oxlint Has vs What It Doesn't
Oxlint ships with 500+ rules built into the binary covering the most common linting needs:
Oxlint built-in rules:
Core JavaScript:
no-unused-vars, no-undef, eqeqeq, no-debugger, no-console,
no-var, prefer-const, no-shadow, no-duplicate-imports,
no-empty, no-unreachable, no-fallthrough, curly
React and React Hooks:
react/jsx-key, react/no-danger, react/no-array-index-key,
react-hooks/rules-of-hooks, react-hooks/exhaustive-deps
TypeScript (syntax-level, no type info needed):
@typescript-eslint/no-explicit-any,
@typescript-eslint/no-unused-vars,
@typescript-eslint/consistent-type-imports
Import/Export:
import/no-duplicates, import/no-self-import (basic)
Unicorn (subset):
unicorn/prefer-array-flat-map, unicorn/prefer-string-trim-start-end,
unicorn/no-array-for-each
What still requires ESLint:
Type-aware TypeScript rules (need type graph):
@typescript-eslint/await-thenable
@typescript-eslint/no-floating-promises
@typescript-eslint/no-misused-promises
@typescript-eslint/no-unsafe-assignment
@typescript-eslint/no-unsafe-call
@typescript-eslint/no-unnecessary-type-assertion
Plugin rules not yet in Oxlint:
eslint-plugin-tailwindcss (class sorting/linting)
eslint-plugin-jsx-a11y (accessibility)
eslint-plugin-import (advanced cycle detection)
eslint-plugin-security
Custom organization-specific rules
The type-aware TypeScript rules are the critical gap. Rules like no-floating-promises (catches unhandled async code) and no-misused-promises (catches Promise passed where boolean expected) require TypeScript's type checker to run. Oxlint has no access to type information — it operates on syntax only.
For most projects, these type-aware rules catch ~10-15% of real errors but are among the most valuable rules in the TypeScript ecosystem. They're worth keeping ESLint around for.
Setup: Oxlint Alone
npm install --save-dev oxlint
// package.json
{
"scripts": {
"lint": "oxlint ."
}
}
Oxlint works with zero configuration — sensible defaults are built in. For custom configuration:
// oxlintrc.json — optional custom config
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"rules": {
"no-unused-vars": "error",
"no-undef": "error",
"eqeqeq": "error",
"no-debugger": "error",
"react/jsx-key": "error",
"react/no-danger": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/consistent-type-imports": "error"
},
"plugins": ["react", "react-hooks", "@typescript-eslint"],
"env": {
"browser": true,
"node": true,
"es2022": true
},
"ignore": {
"patterns": ["dist/**", ".next/**", "node_modules/**"]
}
}
Setup: Optimal Combined Stack (Oxlint + ESLint)
The production-optimal configuration runs oxlint for fast feedback and ESLint only for what oxlint can't do:
npm install --save-dev oxlint eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
// package.json
{
"scripts": {
"lint:fast": "oxlint .",
"lint:types": "eslint --max-warnings 0 .",
"lint": "oxlint . && eslint --max-warnings 0 ."
}
}
Minimal ESLint config focused only on what ESLint does uniquely:
// eslint.config.js — type-aware rules and plugins only
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import tailwindPlugin from 'eslint-plugin-tailwindcss';
import a11y from 'eslint-plugin-jsx-a11y';
import prettierConfig from 'eslint-config-prettier';
export default [
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
plugins: { '@typescript-eslint': tsPlugin },
rules: {
// Only type-aware rules that require type information
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
'@typescript-eslint/strict-boolean-expressions': 'warn',
// Disable the syntax-level rules oxlint already handles
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
plugins: {
tailwindcss: tailwindPlugin,
'jsx-a11y': a11y,
},
rules: {
'tailwindcss/classnames-order': 'warn',
...a11y.configs.recommended.rules,
},
},
prettierConfig,
];
The key principle: turn off in ESLint every rule that oxlint already handles. Duplicate rules running in both tools waste time and can produce conflicting output.
CI Strategy
The recommended CI pattern from Vercel's engineering blog:
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
lint-fast:
name: Oxlint (fast pre-check)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npx oxlint .
# Fails in ~0.5s if there are basic errors
# Developers see feedback before ESLint even starts
lint-full:
name: ESLint (type-aware + plugins)
runs-on: ubuntu-latest
needs: lint-fast # Only run if oxlint passes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm run lint:types
Running oxlint as a prerequisite job means the full ESLint run doesn't even start if there are basic errors — saving CI minutes and giving faster feedback.
Pre-Commit Hook (husky + lint-staged)
npm install --save-dev husky lint-staged
npx husky init
// .lintstagedrc.json
{
"*.{ts,tsx,js,jsx}": [
"oxlint", // Runs on changed files in ~200ms
"eslint --fix" // Type-aware rules — slower, only on changed files
],
"*.{ts,tsx,js,jsx,json,md,css}": [
"prettier --write"
]
}
lint-staged runs both tools only on staged files, which significantly reduces the per-commit overhead compared to linting the whole project.
Comparison Table
| Oxlint | ESLint | Biome | |
|---|---|---|---|
| Speed | 50-100x faster | Baseline | ~25x faster |
| Rule count | 500+ | 10,000+ (w/ plugins) | 300+ |
| Plugin system | None (planned) | Full ecosystem | None |
| Type-aware rules | No | Yes | No |
| TypeScript formatting | No | Via Prettier | Yes |
| Tailwind sorting | No | Via plugin | No |
| Maturity | 2023/growing | 2013/mature | 2023/growing |
| Weekly downloads | ~400K | ~42M | ~1.2M |
Package Health
| Package | Weekly Downloads | Maintained | Language |
|---|---|---|---|
eslint | ~42M | Active | JavaScript |
oxlint | ~400K | Active (Vercel/oxc) | Rust |
@biomejs/biome | ~1.2M | Active | Rust |
When to Choose
Add oxlint to your existing ESLint setup if:
- CI lint times exceed 30 seconds
- You want faster pre-commit feedback without giving up ESLint's type-aware rules
- Your project uses standard React + TypeScript (oxlint's rule coverage is best here)
Use ESLint only if:
- You need full custom plugin ecosystem (security rules, organization-specific rules)
- Lint time is already acceptable and you don't want to manage two linters
- Your team isn't comfortable with an experimental setup
Consider Biome instead if:
- You also want to replace Prettier (Biome handles formatting + linting)
- You don't need Tailwind class sorting or jsx-a11y
- You're starting a new project and want a single Rust-powered tool
The 2026 consensus: oxlint is mature enough to run in production CI alongside ESLint. The two complement each other rather than compete — oxlint handles volume and speed, ESLint handles TypeScript semantics and plugin rules.
ESLint Flat Config: Why It Matters for This Stack
ESLint 9.x made flat config (eslint.config.js) the default, deprecating .eslintrc in all its forms. If you're setting up the oxlint + ESLint dual stack on a new project, use flat config from the start — legacy config support will eventually be removed.
The flat config format has two advantages over the old format that are relevant here. First, you can see exactly which rules are active without tracing through extends chains and plugin precedence rules. This matters when you're deliberately stripping out rules that oxlint already handles — you need to be confident that disabling no-unused-vars in ESLint doesn't silently re-enable it through an extended config. Second, flat config is imported as regular JavaScript modules, which makes it composable: you can split your oxlint-facing config from your type-aware config into separate files and compose them in eslint.config.js.
For projects upgrading from legacy ESLint config, the official migration guide is the reference point. Most migrations can be done in a few hours for typical project configurations. The main gotcha is that overrides (the old way to apply different rules to different file patterns) is replaced by separate config objects with files glob patterns.
Should You Switch to Biome Instead?
Biome is worth considering as a third path. It's a Rust-based formatter and linter that handles both jobs in a single binary. If your project uses Prettier for formatting and ESLint for linting, Biome can replace both simultaneously and is measurably faster than running them separately.
The caveat is the same gap as oxlint: Biome has no support for type-aware TypeScript rules and no plugin system. For projects that need no-floating-promises or Tailwind class sorting, Biome alone isn't sufficient. For projects that don't need those — simple TypeScript APIs, Node.js backends without heavy plugin requirements — Biome is an attractive single-tool solution.
In practice, the decision often comes down to Tailwind. If you use Tailwind CSS and want class ordering enforced by your linter, you need ESLint with eslint-plugin-tailwindcss. Neither Biome nor oxlint supports this, and it's not on either project's near-term roadmap. For Tailwind projects, the oxlint + minimal ESLint stack remains the best performance-correctness tradeoff.
Related: esbuild vs SWC 2026, eslint package health, How to set up TypeScript