TL;DR
The linting space is in flux in 2026. ESLint v9 with flat config is the universal default — 50M+ weekly downloads, every framework supports it. Biome is the fastest all-in-one linter+formatter at near-zero config, and a compelling choice for greenfield projects. OXC (oxlint) is the newest entrant — a Rust-based linter that's 50-100x faster than ESLint — but lacks the rule ecosystem. Most teams should use ESLint v9 or Biome; OXC is worth watching but not production-primary yet.
Key Takeaways
- ESLint v9 introduced flat config (
eslint.config.js) — the old.eslintrcformat is officially deprecated - Biome v1.9+ handles formatting AND linting in one tool (replaces ESLint + Prettier)
- OXC (
oxlint) is 50-100x faster than ESLint but covers ~300 rules vs ESLint's 700+ - Biome has ~200 lint rules (growing), zero JavaScript, written in Rust
- TypeScript support: Biome has first-class TS support; OXC has TS parsing; ESLint needs
@typescript-eslint - Ecosystem: ESLint has 4000+ plugins; Biome is standalone; OXC ecosystem is nascent
ESLint v9: Flat Config Migration
ESLint crossed 50M weekly downloads in 2026. The biggest change in v9 is the flat config system — .eslintrc.* is gone, replaced by eslint.config.js:
Old Way (.eslintrc.json — deprecated)
{
"env": { "browser": true, "es2022": true },
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"no-console": "warn",
"@typescript-eslint/no-unused-vars": "error"
}
}
New Way (eslint.config.js — v9 default)
// eslint.config.js
import globals from "globals";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
// Built-in recommended rules
js.configs.recommended,
// TypeScript support
...tseslint.configs.recommended,
// Global settings
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
project: "./tsconfig.json",
},
},
},
// Your custom rules
{
rules: {
"no-console": "warn",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
},
},
// Ignores (replaces .eslintignore)
{
ignores: ["dist/**", "node_modules/**", "coverage/**", "*.config.js"],
},
];
ESLint v9 with React
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
}
);
ESLint Performance (2026 Benchmarks)
ESLint is JavaScript, not Rust. For large codebases, it's slow:
| Codebase size | ESLint v9 | Biome | OXC |
|---|---|---|---|
| 100 files | 1.2s | 0.08s | 0.03s |
| 500 files | 5.8s | 0.3s | 0.1s |
| 2000 files | 24s | 1.1s | 0.4s |
ESLint supports --cache to skip unchanged files, but cold runs are slow.
Biome: The All-In-One Alternative
Biome was born from Rome Tools' pivot — it's a Rust-based formatter + linter + import sorter that replaces ESLint + Prettier in a single binary.
npm install --save-dev @biomejs/biome
npx @biomejs/biome init
biome.json
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"organizeImports": { "enabled": true },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUndeclaredVariables": "error",
"useExhaustiveDependencies": "error"
},
"suspicious": {
"noExplicitAny": "warn",
"noDoubleEquals": "error"
},
"style": {
"useConst": "error",
"noNonNullAssertion": "warn"
}
}
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": false
},
"formatter": {
"quoteStyle": "double",
"trailingCommas": "es5"
}
}
}
Biome CLI
# Lint and format together
biome check --write .
# Just lint (no formatting)
biome lint .
# Just format
biome format --write .
# CI mode (no changes, just error on violations)
biome ci .
Biome's Rule Coverage
Biome v1.9 ships ~200 lint rules across:
correctness— logic errors, exhaustive deps, unused variablessuspicious— double equals, commented code, console.logstyle— const vs let, arrow functions, template literalsnursery— experimental rules (opt-in)performance— no-barrel-files, no-array-index-key
What Biome doesn't have: Biome has no plugin system. If you need custom rules, Biome can't do it. ESLint's plugin ecosystem (4000+ packages) is irreplaceable for complex projects.
Biome vs Prettier: Compatibility
Biome formats identically to Prettier for ~97% of cases. The remaining 3% are edge cases where Biome made different aesthetic choices. The biome migrate prettier command handles the gap:
npx @biomejs/biome migrate prettier --write
# Reads .prettierrc and applies those settings to biome.json
OXC: Rust-Speed Linting
OXC (JavaScript Oxidation Compiler) is the Rust-based JavaScript/TypeScript toolchain that includes:
- oxc_parser — the fastest JS/TS parser in the ecosystem
- oxlint — linter built on oxc_parser (the installable npm package)
- oxc_transformer — TS/JSX transformer (Babel replacement)
- rolldown uses oxc_parser internally
npm install oxlint --save-dev
npx oxlint .
OXC Speed
OXC's headline claim is 50-100x faster than ESLint. From their benchmarks:
| Command | ESLint | OXC |
|---|---|---|
| Lint TypeScript (cold) | 24.0s (2000 files) | 0.4s |
| Lint React codebase | 9.2s (800 files) | 0.18s |
This is achieved by being written in Rust and using multithreaded parsing.
OXC Configuration
// .oxlintrc.json
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"rules": {
"no-unused-vars": "error",
"no-console": "warn",
"eqeqeq": "error",
"prefer-const": "error"
},
"plugins": ["react", "typescript"],
"env": {
"browser": true,
"node": true
},
"ignorePatterns": ["dist/**", "node_modules/**"]
}
OXC Rule Coverage
OXC supports ~300 rules including ESLint core rules, React hooks rules, and TypeScript rules. The coverage is growing rapidly, but there are gaps:
OXC has:
- ✅ All ESLint core recommended rules
- ✅ React hooks rules (
react-hooks/rules-of-hooks,react-hooks/exhaustive-deps) - ✅ Common TypeScript rules
- ✅ Import/no-cycle (graph-based, very fast)
OXC doesn't have:
- ❌
@typescript-eslintadvanced type-aware rules (requires type checking) - ❌ Custom plugin system
- ❌ Most framework-specific plugins (Svelte, Vue, Astro)
- ❌
eslint-plugin-security,eslint-plugin-sonarjs
OXC as a Speed Layer
A popular pattern is running OXC first (fast, catches obvious issues) and ESLint only for type-aware rules:
// package.json
{
"scripts": {
"lint": "oxlint . && eslint --cache . --rule '@typescript-eslint/no-floating-promises: error'",
"lint:fast": "oxlint .",
"lint:types": "eslint --no-eslintrc -c eslint.types.config.js ."
}
}
Framework-Specific Recommendations
Next.js Project
// eslint.config.js — Next.js uses ESLint by default
import nextPlugin from "@next/eslint-plugin-next";
export default [
...tseslint.configs.recommended,
{
plugins: { "@next/next": nextPlugin },
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
},
},
];
Next.js's eslint-config-next has not been ported to Biome. Use ESLint for Next.js projects.
Vite + React (Biome is a great fit)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install --save-dev @biomejs/biome
npx @biomejs/biome init
# Remove eslint and prettier from devDependencies
Biome replaces ESLint + Prettier perfectly for Vite/React apps without framework-specific lint rules.
Library Development
Use ESLint. Libraries often need:
@typescript-eslinttype-aware ruleseslint-plugin-jestoreslint-plugin-vitesteslint-plugin-jsdocfor API documentation- Custom rules for your library's API patterns
Feature Comparison Table
| Feature | ESLint v9 | Biome | OXC |
|---|---|---|---|
| Speed | ⚠️ Slow (JS) | ✅ Fast (Rust) | ✅ Fastest (Rust) |
| Formatting | ❌ (needs Prettier) | ✅ Built-in | ❌ |
| Import sorting | 🔌 Plugin needed | ✅ Built-in | ❌ |
| Rule count | ✅ 700+ core | ✅ ~200 | ⚠️ ~300 |
| Plugin ecosystem | ✅ 4000+ plugins | ❌ None | ❌ None |
| Type-aware rules | ✅ Full TS support | ⚠️ Limited | ❌ |
| Custom rules | ✅ Full JS API | ❌ | ❌ |
| Vue/Svelte/Astro | ✅ Via plugins | ⚠️ Partial | ❌ |
| Config complexity | ⚠️ Moderate | ✅ Minimal | ✅ Minimal |
| Auto-fix | ✅ | ✅ | ✅ |
| Editor integration | ✅ Universal | ✅ VSCode/JetBrains | ⚠️ Growing |
| Weekly downloads | 50M+ | 3M+ | Growing |
Migration Paths
.eslintrc → ESLint v9 Flat Config
# Official migration helper
npx @eslint/migrate-config .eslintrc.json
This generates an eslint.config.js from your old config. Usually requires manual cleanup.
ESLint + Prettier → Biome
npx @biomejs/biome migrate eslint --write
npx @biomejs/biome migrate prettier --write
Two commands convert your existing configs. Then uninstall ESLint and Prettier.
Recommended Setup by Project Type
| Project | Recommendation | Why |
|---|---|---|
| Next.js app | ESLint v9 + next plugin | Next.js requires ESLint; flat config is clean |
| Vite + React | Biome | No framework-specific rules needed; Biome is faster |
| Node.js library | ESLint v9 | Type-aware rules critical for library quality |
| Monorepo | OXC (fast CI) + ESLint (type rules) | Best of both worlds |
| New SvelteKit project | ESLint + svelte plugin | Biome/OXC don't support Svelte syntax yet |
| Legacy Webpack project | ESLint v9 (migrate to flat config) | Stability over speed |
The Two-Pass Linting Strategy
Running OXC before ESLint has become a productive pattern in 2026 for teams that need speed without abandoning the ESLint ecosystem. The idea is simple: OXC handles the fast, obvious checks — unused variables, no-console violations, eqeqeq, import cycles — in milliseconds, failing the CI pipeline immediately on common mistakes before invoking ESLint's slower type-aware analysis. Developers with simple errors never wait for ESLint's full pass.
The split works because OXC and ESLint address different rule categories. OXC runs as a pre-check covering 80% of errors at roughly 1% of the time cost. ESLint then handles the rules that require TypeScript's type information: no-floating-promises, strict-boolean-expressions, no-unsafe-call, no-unsafe-member-access — rules that OXC cannot implement without invoking the type checker. The pipeline exits on the first failure, so if OXC catches a no-console violation or an import cycle, ESLint never runs at all.
For teams switching to this pattern: configure ESLint to run only type-aware rules (@typescript-eslint/strict-type-checked subset), and let OXC handle everything else. Remove any ESLint rules that OXC already covers — running both for the same rule wastes time and can produce inconsistent messages. The configuration overhead is worth it in codebases over 200 files, where ESLint's cold run crosses the 10-second threshold but OXC finishes in under half a second.
// package.json — two-pass linting
{
"scripts": {
"lint": "oxlint . && eslint --cache -c eslint.types.config.js .",
"lint:fast": "oxlint .",
"lint:ci": "oxlint . && eslint --max-warnings 0 -c eslint.types.config.js ."
}
}
Biome's Plugin Gap and Formatting Opinion
Biome formats identically to Prettier for roughly 97% of code patterns. The remaining 3% shows up in specific edge cases: template literal expressions with complex embedded logic, certain JSX attribute arrangements, and long function signatures. For teams migrating from Prettier, the biome migrate prettier command handles the transition cleanly, and the remaining output differences are minor enough to accept. The practical risk in migration is not format differences — it's discovering that a framework-specific ESLint plugin your codebase relies on has no Biome equivalent.
ESLint's plugin ecosystem is irreplaceable for specialized use cases. eslint-plugin-jsx-a11y enforces accessibility rules that Biome doesn't cover. eslint-plugin-security flags patterns like exec() calls with user input. eslint-plugin-n enforces Node.js API usage constraints. eslint-plugin-vue handles Vue single-file components. None of these exist in Biome, and Biome has no plugin API for implementing them — the design is intentional. Biome's development team maintains all rules internally to guarantee performance and consistency.
The formatting opinion gap is a subtler concern. Prettier's non-configurability is part of its value: there's no point debating formatting because Prettier makes the choice for you. Biome offers some configuration (quoteStyle, trailingCommas, lineWidth, indentStyle), which means teams can end up with inconsistent Biome configurations across projects, reintroducing the formatting discussions that Prettier adoption was meant to end. The recommended approach for teams adopting Biome: treat the default Biome configuration as a standard and resist per-project customization unless a specific requirement forces it.
The result in 2026: Biome is the right choice for TypeScript/React projects without specialized rule requirements that can benefit from a single-tool workflow. ESLint remains the right choice wherever framework-specific plugins, accessibility enforcement, or custom rules are non-negotiable.
Methodology
- Benchmarked on a 2000-file TypeScript React codebase (MacBook M3 Pro + GitHub Actions ubuntu-latest)
- Tested ESLint v9.18, Biome v1.9.3, OXC 0.14.x
- Reviewed OXC roadmap for rule implementation status
- Tested all flat config migration paths with official migration tools
- Analyzed npm download trends for eslint, @biomejs/biome, and oxlint packages
See ESLint vs Biome download trends on PkgPulse — updated in real time.
See also: ESLint vs Prettier and AVA vs Jest, Biome vs ESLint + Prettier: The Linter Wars 2026.