jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

oxlint — Linter siêu nhanh cho JS/TS, và so sánh với ESLint, Biome, Prettier

Giới thiệu và hướng dẫn cơ bản về oxlint: cài đặt, file config, categories, plugins, type-aware linting, migrate từ ESLint, kèm so sánh thẳng với ESLint, Biome và Prettier.

Mỗi frontend engineer đều từng ngồi đợi eslint chạy hàng chục giây trên CI, hoặc nhìn node_modules phình lên vì một rừng plugin và shareable config. oxlint ra đời để giải quyết đúng nỗi đau đó: một linter viết bằng Rust, nhanh hơn ESLint 50–100 lần, có sẵn hơn 800 rule, và gần như cắm-là-chạy.

Bài này đi từ “oxlint là gì” đến đủ để bạn tự tin đưa nó vào project thật, rồi so sánh thẳng thắn với ESLint, BiomePrettier để bạn biết khi nào nên dùng cái nào.

Phạm vi: bài tập trung vào oxlint (linter). Người anh em formatter của nó là oxfmt được nói riêng ở cuối bài. Cả hai đều thuộc hệ sinh thái Oxc (The JavaScript Oxidation Compiler).


oxlint là gì và vì sao nó tồn tại

oxlint là một linter cho JavaScript và TypeScript, xây trên Oxc compiler stack viết bằng Rust. Linter là công cụ phân tích tĩnh (static analysis): nó đọc code mà không chạy, rồi cảnh báo các lỗi tiềm ẩn (bug thật sự, biến không dùng, promise bị bỏ quên…) và các pattern không nhất quán.

Bối cảnh kinh điển là ESLint + một đống plugin (@typescript-eslint, eslint-plugin-react, eslint-plugin-import…) + Prettier để format. Setup này mạnh và linh hoạt, nhưng trên codebase thật nó có ba vấn đề:

  • Chậm. ESLint chạy trên Node, parse lại cây cú pháp nhiều lần, và mỗi plugin là một lớp JS nữa. Lint một monorepo có thể mất hàng chục giây.
  • Config phình to. .eslintrc, eslint.config.mjs, version của hàng chục plugin phải khớp nhau, rule stylistic của ESLint còn đánh nhau với Prettier.
  • Dependency nặng. Mỗi plugin kéo theo cây phụ thuộc riêng.

oxlint đánh vào cả ba: tốc độ Rust, rule có sẵn (không cần cài plugin), và một file config nhỏ gần giống ESLint v8 nên đọc rất quen.


Vì sao oxlint nhanh đến vậy

Tốc độ không phải phép màu, mà đến từ kiến trúc:

  • Viết bằng Rust, biên dịch ra native binary — không có overhead của Node runtime cho phần lõi.
  • Một lần parse, nhiều rule dùng chung. ESLint thường để mỗi rule tự duyệt AST; oxlint chia sẻ kết quả parse và phân tích giữa các rule.
  • Chạy song song theo file (multi-threaded), tận dụng nhiều core — thứ kiến trúc single-thread của Node khó làm tốt.

Kết quả theo benchmark chính thức: nhanh hơn ESLint 50–100 lần. Trong thực tế, lint từ “vài giây” rớt xuống “vài chục mili-giây”, đủ nhanh để chạy mượt trong pre-commit hook mà không ai để ý.


Cài đặt và dùng cơ bản

Cài như một dev dependency:

pnpm add -D oxlint

Chạy ngay không cần config — oxlint hữu ích ngay từ đầu vì mặc định bật nhóm rule correctness (chỉ những lỗi gần như chắc chắn sai):

# lint toàn bộ thư mục hiện tại
pnpm exec oxlint

# tự sửa những lỗi fix được an toàn
pnpm exec oxlint --fix

Thêm script vào package.json cho gọn:

// package.json
{
  "scripts": {
    "lint": "oxlint",
    "lint:fix": "oxlint --fix"
  }
}

oxlint hỗ trợ .js, .mjs, .cjs, .ts, .mts, .cts, .jsx, .tsx, và các file framework .vue, .svelte, .astro (chỉ lint phần <script> bên trong).

Một lần chạy trông như thế nào

Giả sử bạn viết if (status == 'active') và quên một biến không dùng:

$ pnpm lint

  ⚠ eslint(eqeqeq): Expected '===' and instead saw '=='
   ╭─[src/lib/customers.ts:12:9]
   ·         if (status == 'active') {
   ·                    ──
   ╰────
  help: Prefer strict equality '==='

  ✖ eslint(no-unused-vars): Variable 'tmp' is declared but never used
   ╭─[src/lib/customers.ts:3:7]
   ·   const tmp = computeRows(data);
   ·         ───
   ╰────

Found 1 warning and 1 error.
Finished in 41ms on 128 files using 8 threads.

Để ý dòng cuối: lint 128 file trong 41ms — cùng cây code này ESLint thường mất vài giây.

Override nhanh từ CLI

Khi muốn thử nhanh không cần sửa config, dùng cờ -A (allow/off), -W (warn), -D (deny/error). Các cờ áp dụng từ trái sang phải:

oxlint -D no-alert -W oxc/approx-constant -A no-plusplus

File config

oxlint chạy được không cần config, nhưng đa số team sẽ commit một file để local, editor và CI dùng chung một bộ rule. Có hai dạng:

  • .oxlintrc.json — cho phép comment như JSONC, cố ý tương thích định dạng .eslintrc.json của ESLint v8 nên migrate rất êm.
  • oxlint.config.ts — config bằng TypeScript với defineConfig, có autocomplete và type-check (yêu cầu Node v22.18+/v24+).

Tạo file khởi tạo:

oxlint --init   # sinh ra .oxlintrc.json

Một config tối thiểu:

// .oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "categories": {
    "correctness": "error"
  },
  "rules": {
    "eslint/no-unused-vars": "error"
  }
}

Hoặc bản TypeScript tương đương:

// oxlint.config.ts
import { defineConfig } from 'oxlint';

export default defineConfig({
  categories: { correctness: 'error' },
  rules: { 'eslint/no-unused-vars': 'error' },
});

categories — bật cả nhóm rule một lúc

Thay vì bật từng rule, oxlint nhóm rule theo ý đồ. Mặc định chỉ correctness được bật. Các nhóm có sẵn:

CategoryÝ nghĩa
correctnessCode gần như chắc chắn sai hoặc vô dụng (bật mặc định)
suspiciousCode nhiều khả năng sai
pedanticRule cực nghiêm, có thể có false positive
perfRule nhắm cải thiện performance runtime
styleRule về style nhất quán, idiomatic
restrictionCấm một số pattern/feature cụ thể
nurseryRule đang phát triển, có thể thay đổi
{
  "categories": {
    "correctness": "error",
    "suspicious": "warn",
    "pedantic": "off"
  }
}

rules — chỉnh từng rule kiểu ESLint

Mỗi rule nhận một severity ("off"/"allow", "warn", "error"/"deny") hoặc mảng [severity, options]. Nếu rule thuộc ESLint core và tên là duy nhất, có thể bỏ prefix plugin (no-console = eslint/no-console):

{
  "rules": {
    "no-alert": "error",
    "oxc/approx-constant": "warn",
    "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
    "eslint/prefer-const": ["error", { "destructuring": "any" }]
  }
}

plugins — rule có sẵn, không cần cài

Đây là điểm “đã” nhất so với ESLint: các plugin phổ biến được viết sẵn bằng Rust ngay trong oxlint, không cần pnpm add thêm gì. Tổng cộng hơn 830 rule, phủ:

  • ESLint core rules
  • TypeScript rules (kể cả type-aware)
  • react, react-hooks, jsx-a11y
  • import, unicorn, oxc
  • jest, vitest, nextjs

Lưu ý: khai báo plugins sẽ ghi đè bộ mặc định, nên hãy liệt kê đủ những gì bạn muốn bật:

{
  "plugins": ["react", "typescript", "import", "unicorn", "oxc"]
}

overrides — config theo pattern file

Dùng overrides để áp rule khác nhau cho test, scripts, hay file TS-only:

{
  "rules": { "no-console": "error" },
  "overrides": [
    {
      "files": ["scripts/*.js"],
      "rules": { "no-console": "off" }
    },
    {
      "files": ["**/*.{ts,tsx}"],
      "plugins": ["typescript"],
      "rules": { "typescript/no-explicit-any": "error" }
    },
    {
      "files": ["**/test/**"],
      "plugins": ["jest"],
      "env": { "jest": true },
      "rules": { "jest/no-disabled-tests": "off" }
    }
  ]
}

Các field hữu ích khác

  • extends — kế thừa từ config khác (merge theo thứ tự, sau ghi đè trước). Import config từ package thì phải dùng oxlint.config.ts.
  • env / globals — bật global cho môi trường (browser, node…) và khai báo global riêng (readonly/writable/off).
  • settings — config dùng chung cho nhiều rule của một plugin (ví dụ react.linkComponents, next.rootDir).
  • ignorePatterns — bỏ qua file ngay trong config. Ngoài ra oxlint tôn trọng cả .gitignore.eslintignore.
  • options — tùy chọn cấp linter như typeAware, typeCheck, maxWarnings.

Type-aware linting

Một số rule chỉ làm đúng khi biết kiểu. Ví dụ kinh điển là floating promise — gọi một hàm async mà quên await:

async function save() { /* ... */ }

// bug: promise bị "thả nổi", lỗi nuốt mất, thứ tự chạy sai
save();        // ⚠ no-floating-promises
await save();  // ✅

Để bắt được loại lỗi này, linter phải hiểu save() trả về Promise. ESLint làm việc này qua typescript-eslint với type information — chính xác nhưng chậm.

oxlint dùng tsgo (bản port Go của TypeScript compiler, hay gọi là TypeScript 7), nên có đầy đủ ngữ nghĩa type giống hệt TypeScript thật, mà vẫn nhanh. Bật bằng cờ hoặc config:

oxlint --type-aware
// .oxlintrc.json (chỉ ở config gốc)
{
  "options": {
    "typeAware": true,
    "maxWarnings": 10
  }
}

Đây là khác biệt then chốt với Biome: Biome tự viết type inference riêng thay vì dựa vào TypeScript compiler, và độ phủ vẫn đang hoàn thiện — nghĩa là một số rule type-aware có thể chưa chính xác bằng.


Multi-file analysis

Có những rule cần nhìn cả project, không chỉ một file — ví dụ import/no-cycle (phát hiện vòng lặp import). Trên ESLint những rule này nổi tiếng là chậm vì phải tự dựng đồ thị module.

oxlint hỗ trợ multi-file analysis như tính năng hạng nhất: nó dựng module graph toàn project và chia sẻ phần parse/resolve giữa các rule, tránh “vách đá performance” thường thấy.


Tích hợp editor và CI

  • Editor: cài extension Oxc cho VS Code (hoặc plugin tương đương) để lint hiện ngay khi gõ, dùng đúng config mà CI dùng.
  • CI: vì cực nhanh, chỉ cần chạy oxlint như một step. Dùng --max-warnings 0 để fail build khi có warning.
  • Pre-commit: kết hợp husky + lint-staged chạy oxlint --fix trên file đã stage — nhanh tới mức gần như không cảm nhận được:
// package.json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["oxlint --fix"]
  }
}

Migrate từ ESLint

oxlint thiết kế để chuyển đổi êm ái, có hai con đường:

1. Thay hẳn ESLint (khuyến nghị cho đa số project). Dùng @oxlint/migrate để tự convert config ESLint hiện có sang .oxlintrc.json:

npx @oxlint/migrate

2. Migrate dần (cho repo lớn/phức tạp). Chạy oxlint trước cho nhanh, rồi vẫn chạy ESLint nhưng tắt các rule đã trùng bằng eslint-plugin-oxlint:

// eslint.config.js
import oxlint from 'eslint-plugin-oxlint';

export default [
  // ...config ESLint của bạn
  ...oxlint.configs['flat/recommended'], // tắt rule mà oxlint đã lo
];

Cách này giữ CI nhanh trong lúc bạn dần chuyển những rule niche mà oxlint chưa có sang oxlint, hoặc giữ lại trên ESLint.


So sánh: oxlint vs ESLint vs Biome

Trước hết phân biệt cho rõ vai trò để khỏi so nhầm:

  • Linter (tìm bug, enforce rule): oxlint, ESLint, Biome.
  • Formatter (định dạng code): Prettier, oxfmt, Biome.

Biome đặc biệt vì nó vừa lint vừa format trong một binary. Prettier thì chỉ format, không phải linter — nên so Prettier với oxlint là so nhầm phạm trù (xem mục formatter bên dưới).

Bảng so sánh phần linter:

Tiêu chíoxlintESLintBiome (lint)
Ngôn ngữ lõiRustJavaScript (Node)Rust
Tốc độNhanh nhất (50–100× ESLint)Chậm nhấtRất nhanh
Số rule830+ có sẵnVô hạn qua pluginÍt hơn, đang tăng
Hệ pluginNative (Rust) + JS plugin (alpha)Khổng lồ, trưởng thành nhấtHạn chế, không nhận plugin ESLint
Type-awareCó, qua tsgo (TS thật)Có, qua typescript-eslintType inference tự viết, đang hoàn thiện
Config.oxlintrc.json / oxlint.config.ts, kiểu ESLint v8eslint.config.js (flat)biome.json
Format codeKhông (dùng oxfmt)KhôngCó (tích hợp)
Điểm mạnhTốc độ + độ phủ rule + migrate dễHệ sinh thái & rule nicheAll-in-one lint + format

Tóm gọn:

  • Chọn oxlint nếu bạn muốn một linter chuyên dụng nhanh nhất, độ phủ rule rộng, và migrate từ ESLint mượt.
  • Giữ ESLint nếu bạn phụ thuộc vào plugin/rule niche mà oxlint chưa hỗ trợ (oxlint phủ rule giá trị cao, không phải mọi rule plugin hiếm).
  • Chọn Biome nếu bạn ưu tiên trải nghiệm “một công cụ làm cả lint lẫn format”, chấp nhận hệ rule/plugin còn hẹp hơn.

Formatter: oxfmt vs Prettier vs Biome

oxlint không format code — đó là việc của oxfmt, formatter cùng nhà.

pnpm add -D oxfmt

# format
pnpm exec oxfmt
# kiểm tra mà không sửa file
pnpm exec oxfmt --check

Điểm đáng chú ý của oxfmt:

  • Nhanh hơn Prettier ~30 lần, ~2 lần so với Biome.
  • Tương thích Prettier: đã pass 100% conformance test JS/TS của Prettier — nghĩa là output gần như y hệt, chuyển đổi không gây diff ồn.
  • Batteries included: sẵn import sorting, Tailwind CSS class sorting, sắp xếp field package.json, format embedded (CSS-in-JS, GraphQL) — những thứ Prettier cần plugin riêng.
  • Hỗ trợ rất rộng: JS/TS/JSX/TSX, JSON(C/5), YAML, TOML, HTML, Vue, Svelte, CSS/SCSS/Less, Markdown, MDX, GraphQL…

So sánh nhanh:

Tiêu chíoxfmtPrettierBiome (format)
Tốc độNhanh nhấtChậm nhấtNhanh
Tương thích Prettier100% JS/TS conformanceBản gốcGần, có khác biệt nhỏ
Tính năng sẵnImport/Tailwind/package.json sortCần pluginMột phần
Vai tròChỉ formatChỉ formatLint + format

Lưu ý quan trọng: đừng chạy oxfmt và Prettier cùng lúc — chọn một, nếu không chúng sẽ “đánh nhau” mỗi lần save.


Một .oxlintrc.json mẫu cho dự án React + TS

Gộp lại những gì đã học, đây là config thực dụng để bắt đầu cho một app React + TypeScript:

// .oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["react", "typescript", "import", "unicorn", "oxc"],
  "categories": {
    "correctness": "error",
    "suspicious": "warn"
  },
  "options": {
    "typeAware": true
  },
  "rules": {
    "eqeqeq": "error",
    "no-console": "warn",
    "typescript/no-explicit-any": "error",
    "import/no-cycle": "error"
  },
  "ignorePatterns": ["dist", "coverage", "build"],
  "overrides": [
    {
      "files": ["**/*.test.{ts,tsx}", "**/test/**"],
      "plugins": ["vitest"],
      "rules": { "no-console": "off" }
    }
  ]
}

Khi nào nên (và chưa nên) dùng oxlint

Nên dùng khi:

  • CI/lint đang chậm và bạn muốn cải thiện ngay.
  • Project mới, hoặc đang dùng bộ rule ESLint khá tiêu chuẩn (TS, React, import).
  • Monorepo lớn cần lint nhanh và config nhẹ.

Cân nhắc kỹ khi:

  • Bạn phụ thuộc vào plugin ESLint rất niche chưa được oxlint hỗ trợ (lúc này dùng eslint-plugin-oxlint để chạy song song là phương án an toàn).
  • Bạn cần JS plugin tùy biến — oxlint có hỗ trợ nhưng còn ở giai đoạn alpha.

Bài tập

1. Tại sao oxlint nhanh hơn ESLint tới 50–100 lần dù làm cùng một việc?

Lời giải

Vì kiến trúc khác hẳn: oxlint viết bằng Rust (native binary, không overhead Node runtime cho phần lõi), parse một lần và chia sẻ kết quả giữa các rule thay vì để mỗi rule tự duyệt AST, và chạy song song nhiều thread. ESLint chạy trên Node single-thread và mỗi plugin là một lớp JS bổ sung.

2. Bạn cần rule bắt floating promise (save() quên await). Cấu hình oxlint thế nào, và vì sao Biome có thể kém chính xác hơn ở đây?

Lời giải

Bật type-aware linting: oxlint --type-aware hoặc "options": { "typeAware": true }. Rule này cần type information để biết hàm trả về Promise. oxlint dùng tsgo (TypeScript compiler thật) nên ngữ nghĩa type chính xác như TS; Biome tự viết type inference riêng và độ phủ vẫn đang hoàn thiện, nên một số trường hợp có thể chưa chính xác bằng.

3. Repo của bạn dùng một plugin ESLint niche mà oxlint chưa hỗ trợ. Làm sao vẫn tận dụng tốc độ oxlint mà không mất rule đó?

Lời giải

Chạy song song: dùng oxlint làm linter chính cho tốc độ, đồng thời vẫn chạy ESLint nhưng dùng eslint-plugin-oxlint để tắt các rule đã trùng với oxlint. ESLint khi đó chỉ còn chạy đúng rule niche, nên tổng thời gian vẫn nhanh.

4. Đồng nghiệp hỏi “oxlint thay được Prettier chưa?”. Bạn trả lời sao?

Lời giải

oxlint là linter, không format code — nó không thay Prettier. Thứ thay Prettier là oxfmt (cùng hệ Oxc): tương thích Prettier (pass 100% conformance JS/TS), nhanh hơn ~30×, lại có sẵn import sorting và Tailwind class sorting. So oxlint với Prettier là so nhầm phạm trù.


Tóm tắt

  • oxlint là linter Rust trên Oxc, nhanh hơn ESLint 50–100×, hơn 830 rule có sẵn, gần như cắm-là-chạy.
  • Config .oxlintrc.json (kiểu ESLint v8) hoặc oxlint.config.ts; dùng categories, rules, plugins, overrides để điều khiển.
  • Type-aware linting qua tsgo cho ngữ nghĩa TypeScript thật — bắt được floating promise mà vẫn nhanh; đây là lợi thế so với type inference tự viết của Biome.
  • Migrate từ ESLint dễ: thay hẳn bằng @oxlint/migrate, hoặc chạy song song bằng eslint-plugin-oxlint.
  • Cần format thì dùng oxfmt (tương thích Prettier, có sẵn Tailwind/import sort) — đừng nhầm oxlint với Prettier vì chúng khác phạm trù.
  • Quy tắc chọn nhanh: tốc độ + linter chuyên dụng → oxlint; all-in-one lint+format → Biome; rule/plugin niche trưởng thành → ESLint; chỉ format → Prettier/oxfmt.