In a **large enterprise Nx monorepo with more than 68,000 files**, formatting had become a recurring CI tax. nx format took **4m 34s to check the whole workspace and 5m 09s to write it**, making the cost of formatting increasingly noticeable at this scale.

We set out to reduce that overhead by migrating from **Prettier to oxfmt**, the Rust-based formatter from the Oxc project. The result: the full-workspace formatting check dropped from **4m 34s to 17.8s, a 15.4× speedup**, while the write dropped from **5m 09s to 49.8s**.

But getting there wasn't a simple formatter swap. The Angular application relied on formatting behavior that oxfmt couldn't fully replace, so we built a **Prettier-to-oxfmt hybrid approach**: oxfmt handles ~99% of the files, while Prettier remains responsible for the Angular templates that still require it.

In this article, we break down how we migrated a large enterprise Nx workspace from Prettier to a hybrid oxfmt setup, the compatibility issues we encountered along the way, and the performance gains we measured in practice. We’ll also cover the trade-offs of the approach and provide the setup in a reproducible demo.

## Why Two Formatters: oxfmt Picks the Parser by Filename, Prettier by Glob

oxfmt picks its HTML parser from the filename, so it parses an Angular template _not_ named \*.component.html as plain HTML. Prettier lets you override the parser per file glob; oxfmt does not.

```
overrides: [
  {
    files: "*.html",
    options: {
      parser: "angular",
    },
  },
];
```

We kept both formatters behind one yarn format script that routes:

*   **oxfmt**: TypeScript, JavaScript, JSON, SCSS, and \*.component.html.
    
*   **Prettier**: every other .html, where the Angular parser has to be declared by path.
    

## Three Migration Traps: Ignore Conflicts, Import Sorting, and Replacing nx format

Once the file routing was in place, three issues prevented the migration from being a straightforward Prettier-to-oxfmt replacement: ignore-file conflicts, differences in import sorting, and the loss of `nx format` as the workspace-level wrapper. 

1.  **Ignore Conflict**: oxfmt reads `.prettierignore` as well as its own .oxfmtrc, so the two tools now share that ignore file. We moved the Prettier-only rules into a .prettieronlyignore to keep the split clean.
    
2.  **Import Sorting Parity**: oxfmt can't load Prettier plugins, so its native sorting replaces `@ianvs/prettier-plugin-sort-imports`. Two things travelled with the imports they sat above: `eslint-disable` lines and <reference types='vitest' /> declarations. `partitionByComment: true` anchors them. **Parity is not complete: oxfmt orders import** _**statements**_ **but leaves the named imports inside them alone.**
    
    ```
    // in
    import { zeta, alpha, Mid } from "pkg-b";
    import { OnInit, Component, computed } from "@angular/core";
    
    // oxfmt out: statements reordered, specifiers untouched
    import { OnInit, Component, computed } from "@angular/core";
    import { zeta, alpha, Mid } from "pkg-b";
    
    // Prettier + @ianvs out: both levels sorted
    import { Component, computed, OnInit } from "@angular/core";
    import { alpha, Mid, zeta } from "pkg-b";
    ```
    
    Until then we run ESLint's `sort-imports` alongside oxfmt. Once we adopt oxlint, its port of the same rule takes over.
    
3.  **Replacing workspace-level wrapper**: `nx format` runs one formatter, so the moment we needed two it was out, and its file selection went with it. We had planned a formatter swap and ended up rebuilding the wrapper: the same selection flags, the same changed-since-base default, one exit code for both tools.
    

With the hybrid setup in place, the next question was how much of oxfmt's raw performance advantage would survive in the actual CI workflow. 

## 26x on the Engine, 15x in the CI Check

Raw tool runs and end-to-end workflow are two different numbers, and mixing them oversells the result. Check and write are two more: CI waits on the check, a reformat pays for the write. The tables below keep all four apart.

### Single Library vs. Whole Workspace: 20x and 26x

_The theoretical ceiling after swapping the core engine: Prettier and oxfmt called directly, no Nx, no routing script._

#### **Engine Performance: Prettier vs. oxfmt**

Time to check the tree, wall-clock. Both formatters called directly - no Nx, no routing script. Lower is better.

_Source: prettier-vs-oxfmt final benchmark, 2026-09-10 Library = median of 3 runs; workspace = single run. Write mode: 29.4 s vs 1.8 s (16x) and 4 m 55 s vs 11.3 s (26x)._

**Scope**

**Action**

**Prettier (median)**

**oxfmt (median)**

**Speedup**

**Single Library** (5,446 files)

check

35.4s

1.8s

**20×**

**Single Library** (5,446 files)

write

29.4s

1.8s

**16×**

**Whole Workspace** (67,899 files)

check

4m 35s

10.4s

**26×**

**Whole Workspace** (67,899 files)

write

4m 55s

11.3s

**26×**

Library rows are the median of 3 runs; workspace rows are a single run each.

### The Hybrid Split: 0.9% of the Files, 38% of the Time

Whole-workspace check. Top two bars at true scale; below, the hybrid run magnified to show what its 17.8 s is made of.

_Source: combined format benchmark, 2026-09-10, median of 3 runs. Total is one end-to-end hybrid run; tool times are that tool's median inside those runs. Nx measured cold-graph._

oxfmt takes ~99% of the files; Prettier keeps 599 plain HTML files. Those 599 files cost 6.74 s of a 17.8 s whole-workspace check: 38% of the time for under 1% of the files. Per file, Prettier runs about 70× slower than oxfmt here.

**Formatter**

**Files processed**

**Files changed**

**Share**

**Check (s)**

**Write (s)**

**Handles**

**oxfmt**

67,899

50,875

99.1%

10.99

25.49

TS/JS/JSON/SCSS + `*.component.html`

**Prettier**

599

94

0.9%

6.74

12.04

plain non-component `*.html`

**Total (one end-to-end run)**

68,498

50,969

100%

17.79

49.80

—

All rows come from the same runs: the total is the median end-to-end wall-clock of one hybrid run, and the per-tool columns are that tool's median inside those runs, so they need not add up to the total. We flipped singleQuote so every file needed formatting and both setups did real work.

### Nx Format vs. the Hybrid Script: 5× on One Library, 15× on the Workspace Check

_The engine ceiling is 26×; Nx orchestration and the Prettier tail bring the end-to-end check to 15×, and a full rewrite to 6×._

#### Workflow Performance: nx format vs. the Hybrid Script

End-to-end wall-clock, nx format (Prettier) vs the hybrid script, same session. Solid = check; tinted = what write adds on top. Full bar = write.

_Source: combined format benchmark, 2026-09-10. Median of 3 runs per value. Nx measured cold-graph (nx reset before every run); every file forced dirty so both setups do real work. The tinted segment is write minus check, so the whole bar is measured write time._

**Scope**

**Action**

**Nx Format (median)**

**Hybrid Script (median)**

**Real-world Speedup**

**Single Library**

check

68.8s

13.9s

**5.0×**

**Single Library**

write

88.4s

16.4s

**5.4×**

**Whole Workspace**

check

4m 34s

17.8s

**15.4×**

**Whole Workspace**

write

5m 09s

49.8s

**6.2×**

Median of 3 runs per cell, all from one benchmark session. Nx numbers are cold-graph: `nx reset` ran before every timed run, so each one pays a full project-graph compute, the fresh-checkout worst case. A warm daemon narrows the Nx side; the hybrid script never touches the graph, so its numbers don't move.

## Nx 23.2 Ships oxfmt Support

Since [**Nx 23.2**](https://nx.dev/blog/nx-23-2-release), `nx format` detects oxfmt from your config. It does not replace the hybrid script, because **Nx runs one formatter per workspace.** When both configs exist oxfmt takes priority and unsupported files can’t be routed to prettier anymore.

Nx 23.2 provides replacement for file-selection half of our script (`--all`, `--base`/`--head`, `--files`) but not the routing half. Until [**Oxc #17852**](https://github.com/oxc-project/oxc/issues/17852) lands with parser overrides, we will have to route files to two formatters ourselves.

## The Trade-off: One Custom Script to Keep Prettier on the Templates oxfmt Can't Parse

With Nx 23.2 you already get oxfmt support and the speed with it. One thing you don't get is full file coverage. In our case keeping `*.html` naming for some Angular templates was important, so the cost we pay is a custom script and one more formatter to maintain. That is worth it to keep those templates formatted correctly, until oxfmt can pick its parser by glob ([**Oxc #17852**](https://github.com/oxc-project/oxc/issues/17852)); then we can drop Prettier and get back to `nx format`.

## The Demo Repo: format.mjs, .prettieronlyignore, and the oxfmt Config

Everything above runs in a public Nx + Angular workspace:

[**github.com/push-based/oxfmt-nx-demo**](https://github.com/push-based/oxfmt-nx-demo)

Two branches, so the migration reads as a diff rather than a finished state:

**Branch**

**State**

**`yarn format`** **runs**

`main`

**Before**: a mature Prettier setup, plugin and all

Prettier over the whole workspace

`oxfmt-hybrid`

**After**: the hybrid script

oxfmt for ~99% of files, Prettier for the rest

`git diff main oxfmt-hybrid` is the whole migration. The `oxfmt-hybrid` branch carries the hybrid script, both ignore files, the `.oxfmtrc.json`, and a fixture plus copy-pasteable repro for each of the three traps.

## **The Takeaway**

What started as a slow `nx format` step became a broader migration challenge: **how do you introduce a significantly faster formatter into a large enterprise Nx workspace without losing the behavior the existing setup depends on?**

In this case, a full replacement wasn't yet practical. Keeping Prettier for the small subset of Angular templates that still required it, while moving the remaining ~99% of files to oxfmt, gave us a pragmatic middle ground. The result was a **full-workspace CI formatting check reduced from 4m 34s to 17.8s**, while preserving the formatting behavior the application still required.

As oxfmt's Angular parser support evolves, the remaining Prettier dependency can eventually disappear. Until then, the hybrid approach provides the performance benefits available today **while preserving the formatting behavior that oxfmt cannot yet cover.**

**If you're working with a large Nx or Angular environment, you may be seeing similar friction elsewhere: CI steps that have grown from seconds to minutes, tooling that no longer scales with the codebase, or development workflows that have become harder to change without affecting something else. At that scale, those delays can compound across teams, builds, and release cycles.**

[**Explore your setup with us**](https://push-based.io/contact) **to take a closer look at where that time is going, what is driving it, and which changes could make a meaningful difference without compromising the systems already in place.**
