Skip to content

perf(check): run oxfmt and oxlint in one process (experimental) - #2966

Draft
liangmiQwQ wants to merge 4 commits into
voidzero-dev:mainfrom
liangmiQwQ:liang/check-raw-in-process
Draft

liangmiQwQ wants to merge 4 commits into
voidzero-dev:mainfrom
liangmiQwQ:liang/check-raw-in-process

Conversation

@liangmiQwQ

@liangmiQwQ liangmiQwQ commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Experimental. This relies on undocumented behavior of the oxfmt and oxlint CLI entries, so it is a stopgap until oxc exports a CLI API.

Description

Today vp check spawns oxfmt, then oxlint, then oxfmt again after lint --fix. Each of those processes starts Node and imports Vite to load vite.config.ts.

This PR makes vp check spawn a single internal vp check --raw process that runs both tools in-process, so Vite is only imported once there. Output and exit codes are unchanged.

How it works:

  • vp check plans the steps up front and writes them as JSON to the runner's stdin. Stdin is used because lint-staged can pass enough paths to hit command-line length limits.
  • For each step, the runner sets process.argv and imports the tool's dist/cli.js. A query string re-evaluates it for the second fmt pass.
  • A step ends on beforeExit, or when oxfmt calls process.exit(), which is patched during the step. On Node < 24.13.1, oxfmt forces an exit 50ms after it finishes. The exit code is read from process.exitCode.
  • After each step, the runner writes a marker with the exit code to both stdout and stderr, and it stops at the first failing step. vp check splits each stream at the markers and reports each step as it finishes, just like before.
Workload Raw tools main total This PR total main overhead This PR overhead
vp check 113ms 354ms 265ms (−25%) 241ms 152ms (−37%)
vp check --fix 192ms 495ms 333ms (−33%) 303ms 141ms (−54%)

The small project has a vite.config.ts with fmt and lint blocks and 20 TypeScript files. On this repo, vp check packages/cli/src (type check on) goes from 775ms to 693ms.

My Idea:

Honestly, I met a lot of problems while implementing this direction. Oxfmt & Oxlint is not designed for multiple call in the one process. So I used a lot of hacky ways like beforeExit to handle all of them. I don't think this is the right direction if Vite+ treats it as a long-term approach.

However, the benchmark and ~25% performance improvement is still very impressive. That proves reducing multiple vite imports is still a good direction to make vite-plus package faster.

Future Plan

As Oxlint & Oxfmt CLI is not designed for multiple calls in the one process, we may still consider whether it is possible to reduce Vite imports in a multi-process environment. Alternatively, we may request Oxc team to provide exported functions to run CLI with a clear lifecycle, but that is obviously not very practical.

There is also an issue, #2920, related to Oxlint and Oxfmt, caused by circular dependency with unlocked vite-plus versions. This gave me an idea: could we remove the vite-plus dependency from Oxlint and Oxfmt, and pass the config cross-process, like a temp file or env variable or something else? Or, is that possible to remove the child-process in vp lint and vp fmt, and inject the resolveConfig function at that moment?

I hope to take this opportunity to address two issues at once.

🤖 Generated with Claude Code

Runs the oxfmt and oxlint CLIs in one Node process by importing their
`dist/cli.js` per step. Each step ends when the event loop drains
(`beforeExit`) or when oxfmt calls `process.exit()`, which is patched
for the step. The step list comes from stdin as JSON, and a marker with
the step's exit code is written to stdout and stderr after each step.
`vp check` used to spawn oxfmt, oxlint, and oxfmt again after
`lint --fix`, and each process imported Vite to load vite.config.ts.
It now plans the steps up front, sends them to the internal runner, and
reports each step as the runner's stdout/stderr markers arrive. The
runner stops at the first failing step, matching where reporting
returns early.
@fengmk2

fengmk2 commented Oct 10, 2026

Copy link
Copy Markdown
Member

cc @leaysgur @camc314 WDYT

I have a long-term plan to bundle oxlint and oxfmt into the same napi binary, just like rolldown, but this task probably won't actually be started until next year.

@dsonet

dsonet commented Oct 10, 2026 •

Copy link
Copy Markdown

cc @leaysgur @camc314 WDYT

I have a long-term plan to bundle oxlint and oxfmt into the same napi binary, just like rolldown, but this task probably won't actually be started until next year.

NEXT YEAR?
You could dispatch coding agents to do it tonight before you go to sleep.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants