Skip to content

Benchmark bun fmt against Prettier #655

Closed

Description

@HMarzban

Part of #649. Runs after #654 closes (ruling 3 on #649).

Benchmark

Field Value
Metric (1) Exact: files that bun fmt formats differently from Prettier, among files with a Prettier parser, by file type. (2) Format gate wall time, in seconds.
Before Not measured yet (agent in the repo, Step 2b, before any change). CI reference, from the 2026-11-11 milestone review read of the Run Report: 🔍 Lint, format & styles step median 45 s (27–51 s, n = 7, 2026-11-07 to 2026-11-11). That step also runs lint, so the format share is not split out.
After Not measured yet.
Condition A git archive HEAD copy. Named laptop (model, cores, RAM, OS). Bun 1.4.4. Prettier version from bun.lock (3.9.10 at HEAD 0b302b222). Warm runs, no other load. Record whether the #554 Q11 pool is on main. If it lands mid-benchmark, re-take Step 2b.
How to run Step 2b: the gate's Prettier command from scripts/check-ci.ts (the format block) on the gate's file list, n = 5; and bun run check:ci, n = 5. Step 4: bun fmt --list-different on the same list, one exact count. Step 5: bun fmt on the same list, n = 5. Report median and min–max.
Pass rule Adopt only if 0 files differ and the gate saves at least 10 % of the measured check:ci median. Any counted file: do not adopt; take the Decision below. Timing runs in every case.
Guard bun fmt --doctor: every file type formats natively and the Tailwind sort comes from .prettierrc.json. If any file goes back to Prettier, the timing is void. bun fmt --debug-check runs once.
Owner Agent in the repo: Steps 1 to 6 and the Measured comment. Maintainer: the reformat decision.
Claim Adopt: "Formatting the whole docs.plus repo fell from s to s (median of 5, )." Kill: "bun fmt formatted of files differently from Prettier, so docs.plus stays on Prettier."

Why

Bun 1.4.4 added bun fmt, a formatter that reads our Prettier config. Vendor numbers: 286x faster than Prettier 3.9.10 on the Prettier repo, and 9,648 of 9,731 Prettier tests identical. On real projects only 57 of 74 runs (77%) matched Prettier exactly.

Our Prettier is ^3.9.10 (root package.json), the version Bun tests against. Record the resolved version from bun.lock in Setup. It runs in three places:

  • the format gate in scripts/check-ci.ts:166-207, on tracked files;
  • bun run format (prettier --check .) in .github/workflows/quality-lint.yml:26;
  • .lintstagedrc.js, on staged files.

.prettierrc.json:14 loads prettier-plugin-tailwindcss. With that plugin listed, the bun fmt Tailwind sort is already on (Bun docs).

Steps

The working tree is often dirty, so work in a copy: git archive HEAD | tar -x -C "$T", then bun install --frozen-lockfile --ignore-scripts in the copy. Never use a branch or a worktree.

  • 1. Build the gate's file list in the source repo: git ls-files -z, without symlinks, as scripts/check-ci.ts does with lstat. Check that the count equals the gate's file count.
  • 2. In the copy, run the gate's own Prettier command (scripts/check-ci.ts:184-197) on that list. It must pass.
  • 2b. Before, always. Time that Prettier command 5 times on the gate's list. Also time bun run check:ci 5 times. Report median and min–max for both.
  • 3. Run bun fmt --doctor once. Every file type must be formatted natively, and the Tailwind sort must come from .prettierrc.json. If any file is sent back to Prettier, the timing is void.
  • 4. Pipe the list to xargs -0 bun fmt --list-different --ignore-unknown --ignore-path .prettierignore -- in the copy.
    • Count a file only if prettier --file-info <file> gives a non-null inferredParser.
    • List the others, such as the .toml files and bun.lock, as out of scope, not as differences.
    • Group the count by file type.
  • 5. In every case, time bun fmt on the same list, 5 warm runs, median and min–max. Label it "not adoptable" if any file is counted. Run bun fmt --debug-check once.
  • 6. Post a Measured comment in the [Tracking] Bun 1.4.x: release-age guard, bunfig cleanup, and measured bun check, bun fmt and bun test numbers #649 format. State the gate saving and the check:ci median.

Kill rules

The kill rules decide adoption only. Steps 2b and 5 run in every case.

Decision (only if files are counted)

  1. Accept a one-time repo-wide reformat to move to bun fmt? Recommended: no. Stay on Prettier until the real-project match improves, because a reformat conflicts with every open change.

If adopted later

Change the format scripts, .lintstagedrc.js and the scripts/check-ci.ts gate in one change. quality-lint.yml calls bun run format, so the script change covers it. Remove prettier-plugin-tailwindcss only after that.

Activity

  1. added this to the v2.4.0 milestone on Oct 31, 2026
  2. modified the milestones: v2.4.0, v2.3.6 on Nov 9, 2026
  3. HMarzban commented on Nov 12, 2026

    @HMarzban
    CollaboratorAuthor

    Measured, 2026-11-12

    Question: Does bun fmt format any gate file differently from Prettier, and does the format gate save at least 10 % of the measured check:ci median?

    Setup

    Field Value
    Commit cfac48d87 (the issue names 0b302b222; HEAD moved before the run). Every copy was pinned to it.
    Machine Laptop, Mac17,6, 18 cores, 48 GiB, macOS 27.0.1
    Bun 1.4.4
    Prettier 3.9.10, with prettier-plugin-tailwindcss 0.8.1 (both from bun.lock)
    Tree A scratch clone of cfac48d87 outside the repo, then bun install --frozen-lockfile --ignore-scripts, bun x prisma generate and bash scripts/build-extensions.sh. No env files.
    Why a clone scripts/check-ci.ts calls git ls-files. A bare git archive copy fails the format gate and skips both build:ci gates. The clone has the same files as git archive cfac48d87, plus .git.
    Git state origin/main set to cfac48d87 in the clone, so the extension matrix is "none". The tree stayed clean before and after every run.
    #554 Q11 pool Not on main. check-ci.ts runs its gates one after another.
    Load Other work shared the laptop. A run counts only if the 1-minute load average was under 6 at its start. All counted runs started at 3.99 to 5.96.
    Date 2026-11-12, 11:52 to 12:10 UTC

    The #654 Measured draft came first, as ruling 3 on #649 sets the order.

    Method

    • Step 1, file list: git ls-tree -r -z cfac48d87, without mode 120000 entries. That gives 3,108 of 3,116 tracked files (8 symlinks out). It equals git ls-files in the clone without lstat symlinks, file for file.
    • Step 2, gate command: xargs -0 node_modules/.bin/prettier --check --ignore-unknown --ignore-path .prettierignore -- on that list. It passed.
    • Step 2b, before: that command, then bun run check:ci. 1 discarded warm-up each, then 5 timed runs with /usr/bin/time -l. Every check:ci run printed PASS with both build:ci gates passing.
    • Step 3, guard: bun fmt --doctor --no-script, once.
    • Step 4, count: xargs -0 bun fmt --list-different --ignore-unknown --ignore-path .prettierignore --no-script -- on the list. Prettier's getFileInfo (the API form of --file-info) set the scope: files that are not ignored and have a parser.
    • Step 5, timing: xargs -0 bun fmt --check --ignore-unknown --ignore-path .prettierignore --no-script -- on the list. It ran in turn with the Prettier command, 1 discarded warm-up, then 5 runs. Then bun fmt --debug-check once.
    • Step 5 in the issue says "time bun fmt". With no flag, bun fmt writes files, so the timed command is --check, to match the gate's prettier --check.
    • Positive control: a second scratch clone got one format fault in each of 6 files. The files were .ts, .tsx (Tailwind class order), .md, package.json, .scss and .yml.

    Result: differences (Steps 3 and 4)

    Item Count
    Gate list 3,108 files
    Ignored by .prettierignore 216
    No Prettier parser (out of scope) 351
    In scope (Prettier checks them) 2,541
    bun fmt files selected 2,541, the same set
    bun fmt --doctor: formatted natively 2,541; 0 sent back to Prettier
    Files that bun fmt formats differently 0 of 2,541
    bun fmt --debug-check exit 0, 2,541 files

    In scope by Prettier parser, 0 differences in each:

    Parser Files Differ
    typescript (.ts, .tsx) 2,123 0
    markdown 153 0
    babel (.js, .mjs, .cjs) 88 0
    json 52 0
    yaml 45 0
    scss 30 0
    json-stringify (package.json and similar) 25 0
    html 13 0
    css 12 0
    • Out of scope, not differences: 351 files with no Prettier parser. They are 189 .sql, 55 .png, 25 .sh, 4 .toml, bun.lock and 77 others, such as images, fonts, Git hooks and ignore files.
    • Guard: the doctor reads the Prettier config through package.json. It runs prettier-plugin-tailwindcss as a built-in plugin and sends no file to Prettier. The timing is valid.
    • Positive control: both tools listed the same 6 seeded files and exited 1. The formatted output of each file matched byte for byte. Both sorted className="p-4 flex" to "flex p-4".
    • A write run of bun fmt in the second clone printed "Formatted 0 files, 2541 unchanged", before the seeds.

    Result: timing (Steps 2b and 5)

    n = 5 each, 1 warm-up discarded:

    Command Exit Median (s) Min–max (s) Peak RSS, median (MiB)
    Gate Prettier command, 3,108-file list 0 10.61 10.03–11.41 570
    bun fmt --check, same list 0 0.15 0.14–0.16 88
    bun run check:ci (Prettier gate) 0 90.34 89.47–92.59 3,687 (largest single process)
    • Gate saving, median against median: 10.61 − 0.15 = 10.46 s.
    • 10 % of the check:ci median: 9.03 s. The saving is 11.6 % of 90.34 s.
    • Worst case across the ranges: 10.03 − 0.16 = 9.87 s against 10 % of 92.59 s (9.26 s). It still clears the line.
    • The 9.03 s line replaces the fixed 16 s line, as the kill rules say.

    Raw data

    Attached: 655-format.jsonl, 655-checkci.jsonl, 655-fileinfo.json, 655-doctor.json, 655-list-different.txt (empty), 655-positive-control.txt. Each timing line holds the load average and free memory at its start.

    Not measured

    • bun run format (prettier --check .) in quality-lint.yml, and .lintstagedrc.js on staged files.
    • A check:ci run with bun fmt in the gate. The saving comes from the two standalone commands.
    • bun fmt in CI. The CI reference step also runs lint, so it cannot isolate the format share.
    • Files outside the gate list, such as untracked files.

    Verdict

    Adopt rule met: 0 of 2,541 files differ, and the gate saves 10.46 s, 11.6 % of the 90.34 s check:ci median. The maintainer rules the swap.

  4. HMarzban commented on Nov 14, 2026

    @HMarzban
    CollaboratorAuthor

    Ruling, 2026-11-14: adopt

    The maintainer ruled the swap at the v2.3.6 release audit. The adopt rule passed: 0 of 2,541 files differ, and the gate saves 10.46 s, 11.6 % of the 90.34 s check:ci median (Measured).

    The swap is #795, a child of #554. It changes the format scripts, .lintstagedrc.js and the scripts/check-ci.ts gate in one commit. Prettier stays installed until a later change removes it.

    Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions