If your npm run lint takes 20 seconds on a medium-sized Next.js project, you are not alone. ESLint, despite its rich ecosystem, is a JavaScript process — it parses, resolves, and evaluates files one at a time with an overhead that compounds quickly as codebases grow.
Oxlint is a Rust-based linter from the OXC project (backed by VoidZero, Evan You's tooling company). It reached v1.0 stable in August 2025, shipped JS plugin support in alpha in March 2026, and is now used in production at Shopify, Airbnb, Mercedes-Benz, and Zalando. On codebases that previously took minutes to lint, Oxlint completes in under a second.
In this guide you will:
- Understand how Oxlint achieves its speed advantage
- Install and configure Oxlint in a Next.js 15 TypeScript project
- Enable built-in TypeScript, React, and Next.js plugin rules
- Migrate an existing ESLint config with the official migration tool
- Add custom rule overrides and per-directory configs
- Wire everything into a CI/CD pipeline
- Decide when to keep ESLint alongside Oxlint
Prerequisites
- Node.js 20+ and pnpm installed
- A Next.js 15 project with TypeScript (or create one with
pnpm create next-app@latest) - Basic familiarity with ESLint config files
Why Oxlint Is Different
ESLint runs in a Node.js event loop, serializes plugin calls through JavaScript, and traverses each file's AST with a single thread (unless you manually shard with --max-warnings tricks). It is architected for extensibility, not throughput.
Oxlint is built on the OXC compiler stack, which is written in Rust. Its advantages:
| Factor | ESLint | Oxlint |
|---|---|---|
| Language | JavaScript | Rust |
| Threading | Single thread | Multi-threaded (Rayon) |
| Rule plugins | JavaScript | Rust (native) + JS alpha |
| Config format | .eslintrc.* / flat | .oxlintrc.json / oxlint.config.ts |
| Rules count | Unlimited (plugins) | 819+ built-in |
| Typical speed | 20-120s on large apps | under 1s |
The 50-100x speed claim is not marketing: it comes from parallelising file parsing across CPU cores in native code, with zero serialisation overhead between the parser and rule evaluator.
Step 1: Install Oxlint
pnpm add -D oxlintThat's it. Oxlint ships as a single self-contained binary — no peer deps, no plugin packages to resolve.
Run it immediately without any config to see your project's current state:
pnpm oxlint .You'll see output like:
✖ 3 errors and 12 warnings found.
src/app/page.tsx:14:5 error no-unused-vars 'handler' is defined but never used
src/lib/utils.ts:7:1 warn no-console Unexpected console statement
...
Step 2: Create the Configuration File
Oxlint accepts two config formats. The simplest is .oxlintrc.json at your project root.
Create it:
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/config/schema.json",
"plugins": ["typescript", "react", "nextjs", "jsx-a11y", "import"],
"env": {
"browser": true,
"node": true,
"es2022": true
},
"rules": {
"no-console": "warn",
"no-unused-vars": "error",
"typescript/no-explicit-any": "warn",
"react/jsx-key": "error",
"nextjs/no-img-element": "error"
}
}Available built-in plugins
All of the following plugins are implemented natively in Rust — no npm packages needed:
typescript— ports rules from@typescript-eslint/eslint-pluginreact— ports rules fromeslint-plugin-reactandeslint-plugin-react-hooksnextjs— ports Next.js-specific rules (image, script, font, link anti-patterns)jsx-a11y— ports accessibility rules fromeslint-plugin-jsx-a11yimport— ports import ordering and resolution rulesunicorn— ports code quality rules fromeslint-plugin-unicornjest/vitest— test file rulesoxc— OXC-specific additional rules
Step 3: Use the TypeScript Config (oxlint.config.ts)
For larger projects, the TypeScript config format gives you type-safe rule definitions and overrides per directory:
// oxlint.config.ts
import { defineConfig } from "oxlint";
export default defineConfig({
plugins: ["typescript", "react", "nextjs", "jsx-a11y", "import"],
env: {
browser: true,
node: true,
es2022: true,
},
rules: {
"no-console": "warn",
"no-debugger": "error",
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"typescript/no-explicit-any": "warn",
"typescript/consistent-type-imports": "error",
"react/jsx-key": "error",
"react/no-unknown-property": "error",
"nextjs/no-img-element": "error",
"nextjs/no-html-link-for-pages": "error",
"import/no-duplicates": "error",
},
overrides: [
{
files: ["**/*.test.ts", "**/*.spec.ts", "**/*.test.tsx"],
plugins: ["vitest"],
rules: {
"vitest/no-disabled-tests": "warn",
"vitest/prefer-expect-assertions": "warn",
"no-console": "off",
},
},
{
files: ["scripts/**/*.ts"],
rules: {
"no-console": "off",
"typescript/no-explicit-any": "off",
},
},
],
});The overrides array lets you apply different rule sets to test files, scripts, or any glob pattern — the same pattern ESLint users will recognise.
Step 4: Add npm Scripts
Update your package.json:
{
"scripts": {
"lint": "oxlint --fix-suggestions src",
"lint:fix": "oxlint --fix src",
"lint:ci": "oxlint --format github src"
}
}Key flags:
| Flag | Behaviour |
|---|---|
--fix | Auto-fix fixable violations |
--fix-suggestions | Show suggested fixes without applying |
--format github | GitHub Actions annotation format |
--format json | Machine-readable output for tooling |
--deny-warnings | Treat warnings as errors (good for CI) |
--max-warnings N | Fail if more than N warnings |
-c path | Use a specific config file |
Step 5: Migrate From ESLint
If you have an existing ESLint config, use the official migration tool to generate an equivalent .oxlintrc.json:
pnpm dlx @oxlint/migrateIt reads your .eslintrc.* or eslint.config.js, maps rules to their Oxlint equivalents, and writes .oxlintrc.json. Rules without a native Oxlint equivalent are listed in a summary so you know what remains ESLint-only.
Typical output:
✔ Migrated 34 rules to .oxlintrc.json
⚠ 6 rules have no Oxlint equivalent:
- import/order (custom sort config)
- @typescript-eslint/no-floating-promises
- ...
ℹ Run ESLint only for the 6 remaining rules using the eslint-only config.
Running Oxlint and ESLint in parallel (recommended transition)
For the unmigrated rules, run both linters in your lint script. Because Oxlint runs in under a second, the combined time is still far less than ESLint alone:
{
"scripts": {
"lint": "oxlint src && eslint src --config eslint-remaining.config.js"
}
}The Oxlint docs recommend disabling any ESLint rule that Oxlint already handles, to avoid duplicate diagnostics.
Step 6: Next.js-Specific Rules
Oxlint ships built-in rules for Next.js anti-patterns. Enable the nextjs plugin in your config and you get:
| Rule | What it catches |
|---|---|
nextjs/no-img-element | Raw img tags (use next/image) |
nextjs/no-html-link-for-pages | Raw a tags for internal links (use next/link) |
nextjs/no-script-in-head | script inside next/head |
nextjs/no-sync-scripts | Sync script tags blocking render |
nextjs/no-typos | Typos in Next.js data-fetching exports |
nextjs/next-script-for-ga | Using script for GA instead of next/script |
Example in practice — Oxlint will flag this:
// ❌ Triggers nextjs/no-img-element
export default function Hero() {
return <img src="/hero.jpg" alt="Hero" />;
}Fix:
// ✅ Correct
import Image from "next/image";
export default function Hero() {
return <Image src="/hero.jpg" alt="Hero" width={1200} height={600} />;
}Step 7: TypeScript Rules
The typescript plugin ports the most-used rules from @typescript-eslint. Some highlights:
{
"rules": {
"typescript/no-explicit-any": "warn",
"typescript/no-unused-vars": "error",
"typescript/consistent-type-imports": "error",
"typescript/no-non-null-assertion": "warn",
"typescript/prefer-as-const": "error",
"typescript/no-empty-interface": "warn",
"typescript/no-inferrable-types": "warn"
}
}Note that consistent-type-imports is a powerful rule for TypeScript codebases — it enforces import type { Foo } syntax, which the TypeScript compiler can elide more efficiently and which prevents import-cycle issues:
// ❌ Flagged by typescript/consistent-type-imports
import { User } from "./types";
// ✅ Correct
import type { User } from "./types";Step 8: React Hooks Rules
The react plugin includes hooks rules. These are critical in Next.js App Router projects where components switch between Server and Client components:
{
"rules": {
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn",
"react/jsx-key": "error",
"react/no-unknown-property": "error"
}
}rules-of-hooks catches hooks called conditionally or inside loops — one of the most common runtime bugs in React apps.
Step 9: CI/CD Integration
GitHub Actions
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
oxlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Run Oxlint
run: pnpm oxlint --format github --deny-warnings srcThe --format github flag emits annotations directly into the PR diff view — violations appear as inline comments on the relevant line, exactly like ESLint with the eslint-formatter-github package.
GitLab CI
oxlint:
stage: validate
image: node:20-alpine
script:
- corepack enable
- pnpm install --frozen-lockfile
- pnpm oxlint --deny-warnings src
cache:
key: pnpm-$CI_COMMIT_REF_SLUG
paths:
- node_modules/.pnpm-storeBecause Oxlint typically finishes in under one second, it adds negligible time to your pipeline stage.
Step 10: VS Code Integration
Install the Oxlint VS Code extension to see lint errors inline as you type.
Add to your .vscode/settings.json:
{
"oxc.enable": true,
"oxc.configPath": ".oxlintrc.json",
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}Oxlint integrates alongside Biome (for formatting) and the TypeScript language server — they each handle their own concern without conflict.
Step 11: JS Plugins Alpha (March 2026)
The JS plugins alpha released in March 2026 allows writing Oxlint plugins in JavaScript/TypeScript — the same plugin API familiar to ESLint users. This is a bridge for teams whose ESLint plugins have no Rust equivalent yet.
Enable a JS plugin:
{
"plugins": ["typescript", "react"],
"jsPlugins": ["./my-custom-rules.mjs"]
}A simple JS plugin:
// my-custom-rules.mjs
export default {
rules: {
"no-direct-fetch": {
meta: {
type: "suggestion",
docs: { description: "Prefer the api() helper over raw fetch()" },
},
create(context) {
return {
CallExpression(node) {
if (
node.callee.type === "Identifier" &&
node.callee.name === "fetch"
) {
context.report({
node,
message: "Use the api() helper instead of raw fetch().",
});
}
},
};
},
},
},
};Note: JS plugin execution is still slower than native Rust rules, but because Oxlint handles the bulk of rules natively, the total time remains far less than ESLint alone.
Benchmarks: Real-World Numbers
On a Next.js app with 1,200 TypeScript files:
| Tool | Time |
|---|---|
| ESLint (flat config, no cache) | 42 seconds |
| ESLint (with cache) | 8 seconds |
| Oxlint (cold) | 0.7 seconds |
| Oxlint (warm) | 0.5 seconds |
These are representative numbers from teams that have published migration posts. Your numbers will vary based on hardware, file count, and rule set.
When to Keep ESLint
Oxlint does not yet cover every ESLint rule or plugin. Keep ESLint (scoped to only the gaps) when you need:
@typescript-eslint/no-floating-promises— async error catching- Complex
import/orderwith custom sort groups - Custom in-house ESLint plugins not yet ported
eslint-plugin-testing-libraryfor React Testing Library
The recommended strategy in 2026 is Oxlint first, ESLint for the remainder — run them sequentially. The combined time is still 5-10x faster than ESLint alone.
Troubleshooting
"Rule X not found"
Oxlint rule names include the plugin prefix. Use typescript/no-explicit-any, not @typescript-eslint/no-explicit-any.
"File ignored by .gitignore"
By default Oxlint respects .gitignore. Pass --no-ignore to override, or add explicit ignore patterns in .oxlintrc.json:
{
"ignore": ["dist/**", ".next/**", "node_modules/**"]
}Conflicting rules with ESLint
When running both, disable the Oxlint-equivalent rules in your ESLint config to avoid duplicate reports:
// eslint.config.js
export default [
{
rules: {
// Handled by Oxlint — disable here
"no-unused-vars": "off",
"no-console": "off",
"react/jsx-key": "off",
},
},
];Next Steps
- Read the full rule reference to discover all 819+ available rules
- Explore Biome for a complementary Rust-powered formatter (Oxlint lints; Biome formats)
- Set up Prettier + Oxlint if your team prefers Prettier for formatting
- Try the Oxlint playground to test rules on snippets without a project
Conclusion
Oxlint brings sub-second linting to JavaScript and TypeScript projects that previously suffered through tens of seconds of ESLint delay on every save and CI run. With 819+ built-in rules covering TypeScript, React, Next.js, accessibility, and imports — all implemented natively in Rust — most Next.js projects can replace the bulk of their ESLint config today with zero performance regression and dramatic speed gains.
The migration path is clear: install Oxlint, run @oxlint/migrate, handle the small set of unmigrated rules with a lean ESLint fallback, and enjoy lint times measured in milliseconds rather than seconds.
Your developers will notice. Your CI pipeline will thank you.