LabHub

Blog

Formatters and Linters in 2026 — The Real Place of Biome, dprint, Ruff, oxc, Prettier 4, and ESLint v9

한국어English日本語

Prologue — Why We Are Talking About Formatters Again

There was a time when prettier --write . was the whole story. That was around 2020. Five years later the same command had grown to 30 seconds in a monorepo and two minutes on CI. eslint --max-warnings 0 was slower still, and at some point we were seriously debating whether to remove lint from the pre-commit hook.

The Python world had a parallel pain. The classic combo of black, flake8, isort, and pylint was stable but slow. A full pass on a large repo took minutes, and four config files drifted apart.

Then a wave of Rust tooling arrived with a different answer: if the tool is genuinely 100 times faster, every other workflow assumption changes. Ruff proved it in Python in 2022, and Biome (formerly Rome) took the same path in JS and TS. dprint, oxc, ty (a Rust type checker), and uv (a Rust package manager) followed — a sustained pattern of Rust rewrites led by Astral, the Biome team, and small projects like Boshen's oxc.

In 2026, Prettier 4 officially adopts a Rust backend path, ESLint v9's flat config has stabilized, and typescript-eslint v8 collapses into a single package. Biome 2 ships multi-file analysis and import sorting as first-class features, and Ruff is effectively the Python linting default.

This post maps that terrain. Which tool wins where, how far the "rewrite in Rust and everything gets better" claim actually holds, and what your team should pick. Measured trade-offs, not marketing copy.

Headline: tools got fast and unified. But "Biome solves everything" is still not the picture. The choice comes down to which trade-offs you accept.


1. The Map — A 2026 Overview of Formatters and Linters

Start with the big picture: which tools own which language territory in 2026.

Language / areaTraditional stack2026 defaultNext-gen contender
JS / TSPrettier + ESLintPrettier + ESLint v9, Biome 2oxc, dprint
Pythonblack + flake8 + isort + pylintRuff (format + lint)ty (type checker)
Rustrustfmt + clippyrustfmt + clippyalready Rust
Gogofmt + golangci-lintunchangednone
CSS / SCSSPrettier + stylelintPrettier + stylelint, Biome 2dprint, oxc
MarkdownPrettier, remark-lintPrettier, dprintnone
TOML / YAML / JSONPrettierdprint, taplo, yamlfmtnone
Multi-language monorepoPrettier + ESLintdprint (unifying formatter), Biomenone

One-line takes by category

The protagonist of this post is the third category — Rust-written next-gen integrated tools. Where they replace incumbents, where they only supplement, and where they cannot yet go.

Headline: situations differ by language. Python is effectively decided by Ruff. JS and TS still have a live Biome vs Prettier+ESLint debate. Monorepos opened a new lane for dprint.


2. The Rust Rewrite Pattern — Why 10 to 100 Times Faster

Start with the structural reason — not marketing.

Numbers you can measure

Biome's own benchmarks show 25 to 35 times the throughput of Prettier on single-file formatting, with a wider gap on big repositories. Ruff is regularly reported as roughly 100 times faster than flake8 on equivalent rule sets, turning minute-scale checks into single-digit seconds on medium-to-large Python repos.

Patterns from real-world reports:

Why the gap is this large

  1. Language baseline — JS tools run as interpreted code on V8's JIT; Rust tools are native binaries. That alone buys 3 to 10 times
  2. Single-pass parsing — legacy tools do lex, parse, AST transform in multiple passes. Biome, oxc, and Ruff build one lossless concrete syntax tree (CST) and reuse it for every analysis
  3. Real parallelism — Rust leans into data parallelism so files actually run on multiple cores. JS tools are single-threaded; worker pools pay serialization overhead
  4. Memory layout — CST nodes are stored in cache-friendly arrays (rowan, oxc-resolver). Fewer allocations
  5. Startup cost — a Rust binary starts in 1 to 5 ms; Node in 50 to 200 ms. Repeated CLI invocations compound this

Second-order effects of being fast

Headline: the second-order workflow shift matters more than the raw speed. "Too slow to be worth it" was an unspoken constraint that just got removed.


3. Biome 2 — The Frontrunner of Formatter and Linter Fusion

Biome is the most ambitious integration. The name began as Rome (2020, Sebastian McKenzie — the author of Babel), the project paused due to funding, and the community fork shipped as Biome in 2023. Biome 1.0 in 2024, 2.0 in 2025.

What Biome 2 promises

Sample config

{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
  "files": { "ignoreUnknown": true },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": { "quoteStyle": "single", "semicolons": "asNeeded" }
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": { "noUnusedVariables": "error" },
      "style": { "useImportType": "warn" },
      "suspicious": { "noExplicitAny": "warn" }
    }
  },
  "assist": {
    "enabled": true,
    "actions": { "source": { "organizeImports": "on" } }
  }
}

Where Biome shines

Where Biome falls short

Decision matrix

Team situationBiome fit
Greenfield JS/TS, simple rulesvery high
Monorepo, CI speed matters, no Vue/Sveltehigh
Existing Prettier+ESLint, satisfiedweak motivation to switch
Heavy eslint-plugin-* usagenot yet
Big share of Vue / Svelte / Astro / MDXnot recommended

Headline: Biome is the most compelling unified JS/TS tool today. Plugin ecosystem and non-standard file support are still the gaps before it can fully replace Prettier+ESLint.


4. dprint — The Plugin-Driven Multi-Language Formatter

dprint is a different shape. Tiny core, language-specific plugins. It formats JS/TS, JSON, Markdown, TOML, Dockerfile, and SQL out of one tool.

What sets dprint apart

Sample config

{
  "lineWidth": 100,
  "indentWidth": 2,
  "useTabs": false,
  "typescript": {
    "quoteStyle": "preferSingle",
    "semiColons": "asi"
  },
  "json": {
    "lineWidth": 200
  },
  "markdown": {
    "textWrap": "always"
  },
  "toml": {},
  "dockerfile": {},
  "plugins": [
    "https://plugins.dprint.dev/typescript-0.93.0.wasm",
    "https://plugins.dprint.dev/json-0.20.0.wasm",
    "https://plugins.dprint.dev/markdown-0.18.0.wasm",
    "https://plugins.dprint.dev/toml-0.7.1.wasm",
    "https://plugins.dprint.dev/dockerfile-0.3.4.wasm"
  ],
  "includes": ["**/*.{ts,tsx,js,jsx,json,md,toml,Dockerfile}"],
  "excludes": ["**/node_modules", "**/dist"]
}

Where dprint shines

Where dprint falls short

Who actually uses it

Headline: dprint aims to be the one formatter for every kind of text. No linting, formatting only, and it shines in a truly polyglot monorepo.


5. Ruff — The Rust Tool That Took Python

Ruff is the most overwhelming success story. Charlie Marsh (Astral) shipped the first release in 2022; by 2026 it is effectively number one on PyPI download stats for Python developer tooling. Django, Pandas, FastAPI, Pydantic, Apache Airflow, scikit-learn — all major projects adopt Ruff.

What Ruff promises

Sample config

[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = ["migrations", ".venv", "build"]

[tool.ruff.lint]
select = [
  "E", "W",      # pycodestyle
  "F",           # pyflakes
  "I",           # isort
  "N",           # pep8-naming
  "UP",          # pyupgrade
  "B",           # flake8-bugbear
  "C4",          # flake8-comprehensions
  "DTZ",         # flake8-datetimez
  "S",           # flake8-bandit (security)
  "RUF",         # ruff-specific
]
ignore = ["E501"]  # line-too-long handled by formatter
fixable = ["ALL"]
unfixable = []

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]  # allow assert in tests
"__init__.py" = ["F401"]  # allow unused imports

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true

Where Ruff shines — nearly everywhere

Where Ruff has limits and differences

Astral's bigger picture — a Rust rewrite of the Python stack

Charlie Marsh's ambition does not stop at Ruff. Astral is rewriting the entire Python developer tooling chain in Rust.

ToolReplacesStatus (2026)
Ruffflake8, isort, black, pydocstyle, ...stable, broadly adopted
uvpip, virtualenv, pip-tools, pipx, parts of poetrystable, rapidly spreading
tymypy, parts of pyrightalpha / beta
ryepoetry, hatch conceptuallybeing absorbed into uv

The pattern: once one tool gets fast, the whole workflow reorganizes around it. The same pattern is repeated by Biome and oxc in JS.

Headline: Ruff is the de facto default of the Python developer tooling chain. The reasons are simple — fast, integrated, low migration cost.


6. oxc — The Rust JavaScript Toolchain Family

oxc takes a different shape of ambition. Led by Boshen, the project rewrites the entire JavaScript toolchain in Rust — parser, resolver, transformer, linter, formatter, minifier. Where Biome is a "single-user tool," oxc is closer to "a component library other tools consume."

What oxc ships

ComponentRoleCompares to
oxc-parserJS/TS parseracorn, esprima, swc parser
oxc-resolvermodule resolutionenhanced-resolve
oxc-transformerJS/TS transformBabel, swc
oxlintlinterESLint
oxc-prettierformatter (Prettier Rust port)Prettier
oxc-minifierminifierterser, swc, esbuild

oxlint — the Rust linter aimed at ESLint

oxlint reimplements ESLint core rules and parts of major plugins in Rust. As of 2026, oxlint supports parts of an ESLint v9 compatibility mode and is positioned as a fast pre-flight check.

# Fast lint with oxlint, deep lint with ESLint
oxlint --deny-warnings src/
# ESLint-compatible reporting
oxlint --rules-include=react-hooks src/

Where oxc shines

Where oxc has limits

Biome vs oxc — what differs

AxisBiomeoxc
Goalunified user toolcomponent library + tool family
Single configyes (biome.json)per-tool
User experience"replace Prettier+ESLint""speed up the ESLint workflow"
Pluginsmostly built-in(planned) WASM plugins
Who adoptsapplication teamsbuild tool / framework authors

Headline: Biome targets users, oxc targets tool builders. From a developer's seat Biome is intuitive; from a build-tool author's seat oxc is the practical pick. They live on different layers, not in direct competition.


7. Prettier 4 — How the Reigning Tool Responded

Prettier has been the de facto JS/TS formatter since 2017. Strong opinions, almost no configuration — that is the core philosophy. As a result, nearly every project adopted it.

But the speed problem accumulated. Through version 3, Prettier was pure JS on Node. As the gap to native tools widened, the Prettier team made a Rust backend path official in 2025.

Headline changes in Prettier 4

Sample config

{
  "experimentalRustBackend": true,
  "printWidth": 100,
  "singleQuote": true,
  "semi": false,
  "trailingComma": "all",
  "plugins": [
    "prettier-plugin-tailwindcss",
    "prettier-plugin-organize-imports"
  ]
}

Why Prettier survives

Prettier vs Biome — choosing in 2026

AxisPrettier 4Biome 2
JS/TS format speed5 to 15 times (Rust backend)25 to 35 times
Lint integrationnone (ESLint separate)integrated
Plugin ecosystemhugesmall
Non-standard file supportbroadnarrow
Single configpartial (.prettierrc + .eslintrc still)full (biome.json)
Migration costnone (already in use)medium (biome migrate)

Headline: Prettier is not going away — the ecosystem is too deep. The Rust backend narrows the speed gap, and pairing with ESLint keeps it competitive. Biome shines when "integration value" matters on a new project.


8. ESLint v9 and typescript-eslint v8 — The Flat Config Era

The ESLint camp did not stand still.

What ESLint v9 changes

Sample flat config

// 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(
  { ignores: ['dist', 'build', '.next'] },
  js.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': 'warn',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    },
  }
)

typescript-eslint v8 — a single package

Before v7 you installed @typescript-eslint/parser and @typescript-eslint/eslint-plugin separately. v8 collapses to a single typescript-eslint package. Config gets shorter, too.

npm i -D typescript-eslint

projectService — cutting the cost of type-aware lint

Type-aware lint (@typescript-eslint/recommended-type-checked) is precise but slow. v8's projectService works like the TypeScript LSP — discovers tsconfig automatically and lazy-loads type information per file. Lint speed improves 2 to 5 times on monorepos.

Why ESLint survives

The pattern of coexisting Biome and ESLint

Many teams arrive at this:

Format:           Prettier (or Biome format)
Fast lint:        oxlint / Biome lint  (pre-commit, immediate feedback)
Deep lint:        ESLint                (CI, type-aware + plugins)

ESLint is not being replaced — the workflow is split into a fast first pass and a deep second pass. Biome and ESLint running side by side may look odd, but it works in practice.

Headline: ESLint v9 settled with flat config, and typescript-eslint v8's projectService closes the speed gap. The plugin ecosystem keeps it alive for the foreseeable future.


9. stylelint and the CSS Tooling Picture

CSS follows a similar pattern.

stylelint v16

Who formats CSS well

ToolCSSSCSSLessTailwind class sorting
Prettierstablestablestablevia plugin
Biomestablepartial (2.x)unsupportedunsupported (directly)
dprintbeta pluginbetaunsupportedunsupported

Decision guide

Headline: Prettier + stylelint remains the safest combination for CSS. Biome 2 is catching up fast, but SCSS and Tailwind integration need more time.


10. The Comparison Matrix — One View of 2026

AxisPrettier 4Biome 2dprintRuffoxlintESLint v9stylelint v16
LanguagesJS/TS/CSS/MD/YAML/JSON/Vue/Svelte/AstroJS/TS/JSX/JSON/CSS/GraphQLJS/TS/JSON/MD/TOML/DockerfilePythonJS/TSJS/TSCSS/SCSS/Less
Formatteryesyesyes (core)yesnono (deprecated)no
Linternoyesnoyesyesyesyes
ImplementationJS + Rust (v4)RustRust + WASMRustRustJSJS
Relative speed1x (v4 Rust 5 to 15x)25 to 35x10 to 30x50 to 100x50 to 80x1x1x
Single configpartialfullfull (format only)fullpartialpartialpartial
Plugin ecosystemhugesmallmediumnone (built-in)smallhugehuge
Auto-fixformat onlyformat + some lintformat onlybroadpartialbroadpartial
Editor integrationbestgoodgoodbestgoodbestgood
Adoption cost (existing team)none (already in use)mediummediumlowlow (complement)none (already in use)none
Recommended useall JS/TS, broad file formatsJS/TS integrated, speed firstmulti-language monorepoalmost all Pythonfast first-pass lintdeep lint, plugin ecosystemall CSS

One-line summaries


11. Migrations — Three Real Scenarios

Scenario A · Prettier + ESLint to Biome 2 (Next.js monorepo)

# 1. Install and initialize Biome
npm i -D --save-exact @biomejs/biome
npx biome init

# 2. Migrate Prettier config
npx biome migrate prettier --write
# 3. Migrate ESLint config (where mappable)
npx biome migrate eslint --write

# 4. First formatting pass (one large diff)
npx biome format --write .

# 5. Lint pass
npx biome lint .

# 6. Update CI / package.json scripts
# "lint": "biome lint .",
# "format": "biome format --write ."

Field report:

Signs you should not migrate:

Scenario B · black + flake8 + isort to Ruff (Django monorepo)

# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
extend-select = ["E", "W", "F", "I", "B", "UP", "DTZ", "S", "RUF"]
ignore = ["E501"]

[tool.ruff.format]
quote-style = "double"
# 1. Install
uv add --dev ruff

# 2. Configure pyproject.toml (as above)

# 3. First format pass (95% match with black, one diff)
ruff format .

# 4. Lint + auto-fix
ruff check --fix --unsafe-fixes .

# 5. Simplified pre-commit
# - id: ruff
#   args: [--fix]
# - id: ruff-format
# (previously: black, flake8, isort, pyupgrade, autoflake — 5 entries)

Field report:

Signs you should not migrate: very few. Ruff is effectively the Python default.

Scenario C · Polyglot monorepo with dprint + Biome + Ruff (split workflow)

A big-company monorepo lives with TS, Python, Markdown, TOML, Dockerfile, and Kubernetes YAML side by side. No single tool covers all of that, so split the work.

Format:
  TS / JS / JSON / CSS    -> Biome format
  Python                   -> Ruff format
  Markdown / TOML / Docker -> dprint

Lint:
  TS / JS                 -> Biome lint (fast) + ESLint (deep, CI)
  Python                  -> Ruff
  CSS                     -> stylelint

CI stages:
  Stage 1: dprint check + biome check + ruff check  (all 5 to 10 seconds)
  Stage 2: ESLint --max-warnings 0                   (CI only, 1 to 2 minutes)
  Stage 3: type checking (tsc, mypy / ty)

Field report: three tools instead of one, but each tool is placed where it shines. Pre-commit runs only the fast tools; the deep checks live on CI.

Headline: migrations do not have to be all-or-nothing. Adopt the fast tool where it wins and keep the incumbent for deep verification. This staged path is the safest.


12. Anti-Patterns and Pitfalls

Common traps when adopting fast tooling.

"Biome alone will solve everything"

"Ruff format equals black 100%"

"Editing gets worse without Prettier"

"Rust tools are fast — no caching needed"

"Linter is fast — turn every rule on"

"Pre-commit should run everything"

"Biome migrate will be perfect"

"Single tool equals single source of truth"

"Rust backend means uniformly faster"

"Integrating formatter + linter avoids conflicts"


13. The Decision Tree — What Should Your Team Pick

Start:
|
+- Language is Python?
|   +- Yes -> Ruff (almost always). Keep mypy / ty separate.
|
+- Primarily JS/TS?
|   |
|   +- Greenfield, simple rules, speed-first
|   |   +- Biome 2 standalone
|   |
|   +- Existing Prettier+ESLint, satisfied, no speed pain
|   |   +- Keep it (consider enabling Prettier 4 Rust backend)
|   |
|   +- Existing Prettier+ESLint, real speed pain
|   |   +- Heavy plugin usage -> Prettier + Biome lint (fast first) + ESLint (deep)
|   |   +- Light plugin usage -> Migrate to Biome 2
|   |
|   +- Heavy Vue / Svelte / Astro
|   |   +- Stay on Prettier (Biome cannot cover)
|   |
|   +- Need a faster pre-commit
|       +- Add oxlint to the husky pre-commit
|
+- Polyglot monorepo (TS + Python + infra)
|   +- Split tools:
|      - TS -> Biome or Prettier
|      - Python -> Ruff
|      - infra (TOML, Dockerfile, MD) -> dprint
|
+- CSS / Tailwind heavy
|   +- Prettier + prettier-plugin-tailwindcss + stylelint
|
+- Monorepo-wide lint takes minutes
    +- Add a fast first pass:
       - Biome / Ruff / oxlint as the husky pre-commit and CI stage 1
       - Keep the existing tool for the deep CI pass

Simple rules


Epilogue — Checklist, Anti-Patterns, Next Post

The 2026 picture is different. The Rust rewrite wave bought 10 to 100 times speed, and the "formatter absorbs the linter" pattern finally materialized. Biome 2 and Ruff sit at near-standard status in their domains, Prettier 4 replied with a Rust backend, and ESLint v9 cleaned house with flat config.

But the picture is still not "one tool does it all." Plugin ecosystems, non-standard file formats, and type-aware analysis keep incumbents alive. The right pick for your team starts from measurement — what is the real bottleneck, and which trade-offs you accept.

Tool selection checklist

  1. Is your lint and format time actually a problem? — measure what changes when 30 seconds becomes 1 second
  2. Are the per-language stories different? — Python is mostly settled; JS/TS is not
  3. Do you depend deeply on plugins? — heavy eslint-plugin-* use keeps ESLint
  4. Monorepo, polyglot, non-standard files? — consider dprint or split tools
  5. Migrate all at once or incrementally? — large migrations belong in a dedicated PR
  6. Pre-commit versus CI separation — fast tools belong on pre-commit, deep ones on CI
  7. Do you need type-aware lint? — keep typescript-eslint for no-floating-promises and friends
  8. Does editor integration actually work? — verify VS Code / JetBrains extension maturity
  9. Do you cache tools on CI? — even fast tools have a download step worth caching
  10. Is there team agreement? — partial adoption produces conflicting commits

Anti-pattern recap

Anti-patternWhy it bitesInstead
Dropping ESLint entirely for Biometype-aware rules and plugins vanishsplit fast lint and deep lint
Assuming Ruff format equals black 100%95% match, edge-case diffsdedicated reformat PR
Enabling every rule (ALL)false-positive explosion, team ignoresstart from recommended, add what you need
Pre-commit runs everythingminute-scale commits, --no-verify workaroundsfast on pre-commit, deep on CI
Skipping caching for Rust tools30 to 60 seconds of download costcache binaries and plugins
No diff review post-migrationlost rules unnoticedhuman-review the migration diff
Single tool without team buy-inpartial adoption, diff conflictsagreement first, enforce via tooling
Optimizing for micro-benchmarksnot your actual workflowmeasure full monorepo timings
Format and lint stylistic clashinfinite loopone formatter, drop stylistic lint rules
Treating Prettier 4 Rust backend as production-ready immediatelybeta volatilityconfirm stability before adopting

Next post

Next: "Build Tools in 2026 — The Real Place of Vite, Turbopack, Rspack, Rolldown, esbuild, swc, and oxc". If this post was about how we tidy code, the next is about how we bundle it. esbuild's Go, swc and Turbopack's Rust, Vite's transition to esbuild plus Rolldown, Rspack's Rust rewrite, and oxc's componentization — the same Rust rewrite pattern, applied to bundlers.


References

Comments

No comments yet.

Sign in to leave a comment