Skip to main content

Oxlint vs ESLint: Rust-Powered Linting Performance 2026

·PkgPulse Team
0

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:

  1. 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.

  2. 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.

  3. 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.

  4. No type-checking by default. ESLint's TypeScript-aware rules (@typescript-eslint with parserOptions.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

OxlintESLintBiome
Speed50-100x fasterBaseline~25x faster
Rule count500+10,000+ (w/ plugins)300+
Plugin systemNone (planned)Full ecosystemNone
Type-aware rulesNoYesNo
TypeScript formattingNoVia PrettierYes
Tailwind sortingNoVia pluginNo
Maturity2023/growing2013/mature2023/growing
Weekly downloads~400K~42M~1.2M

Package Health

PackageWeekly DownloadsMaintainedLanguage
eslint~42MActiveJavaScript
oxlint~400KActive (Vercel/oxc)Rust
@biomejs/biome~1.2MActiveRust

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

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.