Skip to content

JavaScript acceleration

In Userspace, commandInterceptors: "auto" (the default) runs eligible JavaScript processes in browser workers. This is the fast path. Linux still owns their files, sockets, process identity, permissions and signals. Other processes run as ordinary Linux programs.

Use your project’s normal commands. OxideVM checks each process before it starts, including processes started by npm, pnpm and Bun. A package manager and the application it starts receive separate decisions.

Open a terminal in the directory whose package.json defines dev. Choose the command your project uses:

Terminal window
npm run dev
Terminal window
pnpm run dev
Terminal window
pnpm dev
Terminal window
bun run dev
Terminal window
bun dev

Run one server at a time. Edit a source file to use the framework’s normal hot reload. Press Ctrl+C to stop the server, then wait for the prompt before starting it again.

The package manager reads your scripts and applies its own lifecycle-hook rules. Arguments, environment variables and working directories keep their normal meaning. npm dev is not an alias for npm run dev; npm reports its usual error.

For SDK applications, use vm.spawn for a development server. See Previews to open its HTTP output.

Why can Bun use the fast path while npm or pnpm stays in Linux?

Section titled “Why can Bun use the fast path while npm or pnpm stays in Linux?”

Bun can resolve a script to an eligible application under /workspace. npm and pnpm have their own JavaScript launchers, usually installed outside /workspace. Those launchers also need qualified image code roots.

Changing the working directory does not qualify an installed launcher. A newer SDK alone does not change the contents of a saved image. Image authors can declare the installed packages, then ship the SDK, engine and image together. Existing workspaces adopt a new image through the explicit image-update flow.

Use the process trace to check the launcher and the application separately. One browser worker does not prove that both processes accelerated.

A node or bun launch runs on the fast path when all of these hold:

  • The runtime is one the engine qualifies: Node.js 24.18.1 at /usr/bin/node or Bun 1.3.14 at /usr/local/bin/bun, recognized by their exact bytes.
  • The command line resolves to a supported script run: for example, node server.js, optionally with --enable-source-maps, or a supported script launched through Bun or a package manager.
  • The resolved script is in an allowed code location: a /workspace project or a qualified image package. A path declaration alone does not qualify a package. The engine checks its immutable image content, permissions and links.
  • The dependencies can be captured safely. The engine finds node_modules from the script and working directory, supported NODE_PATH folders, and linked stores and workspace packages. Installed dependency directories stay read-only for that process. Project source stays editable. Tool caches such as .vite and .cache stay writable. Package stores and launchers under .pnpm, .bun and .bin stay frozen.
  • Every native add-on in those folders that Linux could load is one the engine recognizes by its exact SHA-256: Rollup 4.64.2, SWC 1.15.47 and 1.16.13, and the Next.js 16.2.12 SWC binding. Add-ons built for other systems, such as macOS, Windows or Arm, are ignored, because Linux could not load them either.

The same checks apply to child processes. An accelerated parent does not authorize arbitrary child code. Image package roots include hidden files and remain immutable. Declaring roots does not add them to Node’s package-search paths.

Stop the process before changing installed dependencies, then restart it to use the new dependency generation. Ordinary source edits remain live for HMR.

Development-command qualification does not cover accelerated package installation or tools installed or updated globally at runtime. Run installation in Linux, for example:

Terminal window
OXIDEVM_ACCELERATION=off pnpm install

An accelerated node handles signals the way Node v24 does. Its helper in the image clears the signal mask it inherited and resets inherited ignored signals at startup (SIGPIPE and SIGXFSZ stay ignored, as in Node). SIGINT and SIGTERM are always caught, and any other signal only while JavaScript listens for it with process.on.

Which signals process.on can listen for depends on the image’s helper, native bridge 15 or 16. An image built by this SDK says bridge\t16 in its acceleration profile. An image built by an earlier SDK has no such row and has bridge 15; that includes the default images until they are rebuilt with the new helper:

Bridge process.on can listen for
16 Every standard signal Node lets JavaScript catch: 1 to 31 except SIGKILL, SIGSTOP, SIGILL, SIGTRAP, SIGBUS, SIGFPE and SIGSEGV, including SIGQUIT, SIGUSR1 and SIGUSR2
15 SIGHUP, SIGINT and SIGTERM. A listener for any other signal throws FEATURE_NOT_AVAILABLE with reason: "native-bridge-signal-listener". Rebuild the image with the current SDK to listen for it.

An ineligible process runs in Linux with a reason in the trace. These are common reasons and the next checks to make:

Reason What to check
entry-outside-workspace The script is outside /workspace and the qualified image roots. Put project source in /workspace, or use an image that declares the installed tool’s package.
image-code-unavailable A declared path is missing or is not a directory. Check the installed layout and rebuild the image.
image-code-untrusted A declared root is mutable or cannot be verified. Image authors must check its backing, ownership and permissions.
addon-unqualified A native add-on is not qualified. Run the project in Linux.
runtime-unavailable, runtime-hash-mismatch The Node.js or Bun build differs from the qualified runtime.
host-untrusted The image’s Node-API host is not a qualified build.
unsupported-invocation Runtime flags or the invocation form are unsupported. Keep the command in Linux if it needs them.
unsupported-environment An environment setting cannot be reproduced, for example unsupported NODE_OPTIONS.
dependency-escape A dependency link leaves the permitted project, store or image roots. Check both the link and its final target.
root-capture-limit, root-capture-unsupported The scan exceeds its bounds or contains an unsupported filesystem object. Inspect the dependency and image trees.
command-capacity The limit of 32 accelerated commands is reached. Stop an existing process.
remembered-failure This invocation previously stopped on a recognized fast-path limit. See below.
placement-override The command inherited OXIDEVM_ACCELERATION=off.

The full list is the AccelerationRejectionReason type in @oxidevm/sdk/diagnostics.

When a program needs full Linux after it started

Section titled “When a program needs full Linux after it started”

Some unsupported behavior appears only after a process starts. Examples include an unavailable built-in or API, an unqualified add-on, or a worker thread started from source text. The process fails explicitly. OxideVM never automatically replays it in Linux, because it might already have changed files or answered requests.

For a recognized fast-path limitation, the terminal explains the limit and OxideVM remembers the failed invocation. Its next matching run uses Linux. Other unsupported errors also fail explicitly, but are not necessarily remembered. Normal application errors retain their exit status.

Remembered failures are bound to the runtime, host, entry content, arguments, working project, profile and root content. Changes to those inputs, dependencies, or the engine/SDK cause a new decision. An invalid npm dev command does not suppress a later npm run dev launch.

In the Playground, a recognized limitation can offer Run again, which types the same command at the tab’s prompt. You can also choose Linux explicitly with the environment override below.

The records stay in this browser, per origin. eraseLocalData removes them with the rest of the origin’s OxideVM data.

Your app can show recognized limitation notices. Pass an existing Userspace VM, a display callback and an abort signal:

notices.ts
import type { UserspaceVM } from "@oxidevm/sdk";
import { watchAccelerationNotices } from "@oxidevm/sdk/diagnostics";
export function showAccelerationNotices(
vm: UserspaceVM,
showBanner: (message: string) => void,
signal: AbortSignal,
): Promise<void> {
return watchAccelerationNotices(vm, (notice) => showBanner(notice.message), { signal });
}

The promise settles when the signal aborts or the VM ends. Await it or handle its rejection. It fails with FEATURE_NOT_AVAILABLE when acceleration is off.

To run one command and everything it starts in Linux while the fast path stays on for the rest, set OXIDEVM_ACCELERATION=off in its environment:

Terminal window
OXIDEVM_ACCELERATION=off npm run dev

Any value other than auto or an empty value means the same.

Enable tracing before starting your command. Read it after the launcher and application have started, or after you stop the command:

placements.ts
import type { UserspaceVM } from "@oxidevm/sdk";
import { configureJSProcessTrace, readJSProcessTrace } from "@oxidevm/sdk/diagnostics";
export function startPlacementTrace(vm: UserspaceVM): Promise<void> {
return configureJSProcessTrace(vm, 4096);
}
export async function showPlacements(vm: UserspaceVM): Promise<void> {
const trace = await readJSProcessTrace(vm);
if (trace.dropped > 0n) console.warn("The trace is incomplete. Capture a shorter run.");
for (const row of trace.placements) {
console.log(row.entry, row.placement, row.placement === "browser" ? row.basis : row.reason);
for (const root of row.codeRoots ?? []) {
console.log(root.kind, root.path, root.contentSHA256);
}
}
}

A browser row carries basis: "qualified" for qualified native add-ons, or "no-addons". A Linux row carries its reason. trace.failures lists recorded fast-path limitations. The trace is bounded, so check trace.dropped before treating it as complete.

For npm or pnpm, look for both the installed CLI entry and the application’s resolved entry. A successful result has a browser row for each eligible process. Every child is checked separately.

New placement records also list the selected codeRoots. Image roots include contentSHA256; browser rows include the captured dependencyGeneration. Historical traces can omit codeRoots. These fields help image authors check which package tree and generation the engine admitted.