← All writing

9 min read

Why I’m Standardising My JavaScript Projects on Bun

Performance pushed me to move 14 repositories to Bun. Here is what improved, where compatibility still matters, and why Anthropic and the Rust rewrite changed the long-term calculation.

I have started standardising most of my JavaScript and TypeScript projects on Bun. At the time of writing, 14 of the 22 repositories I own use it on their default branch.

That figure comes from checking the repositories themselves rather than counting migration pull requests. It includes conventional Bun projects, mixed toolchains and one repository that uses Bun only for scheduled CI automation.

The main reason is performance.

Not benchmark-chart performance in isolation, although Bun has plenty of that. I mean the cumulative cost of dependency installs, process startup, TypeScript execution, test runs, Docker layers and CI jobs across a collection of projects that are opened, updated and rebuilt repeatedly.

JavaScript tooling often treats a few seconds as irrelevant. Across one command, it usually is. Across every feedback loop in 14 repositories, it becomes part of the architecture.

Install speed changes how often I am willing to start clean#

Package installation is the most obvious difference.

Bun's current documentation describes bun install as up to 25 times faster than npm install. Its published comparison records these installation times on the benchmark machine:

Bar chart comparing Bun, pnpm, Yarn and npm package-install times

Package manager Published time
Bun 0.226 seconds
pnpm 2.335 seconds
Yarn 4.356 seconds
npm 5.653 seconds

Those are Bun's own numbers, not a universal law. They use a particular machine, project and command configuration. Cold caches, native dependencies, lifecycle scripts, network conditions and operating systems can change the result substantially.

The useful point is not that every install will be exactly 25.06 times faster. It is that Bun is designed to make dependency installation cheap enough that deleting node_modules, rebuilding a container layer or starting a fresh CI runner is less painful.

The implementation choices behind that matter more than the headline. Bun keeps a global package cache, resolves and extracts packages in parallel, and uses platform-specific filesystem operations. On Linux it can hardlink cached packages; on macOS it uses clonefile. Its isolated linker can also use a global content-addressed store rather than reproducing every package from scratch inside every project.

That changes behaviour. I am more likely to verify a clean installation, less reluctant to invalidate a Docker cache and less tempted to preserve a questionable node_modules directory because rebuilding it feels expensive.

Fast clean builds are not merely convenient. They make reproducibility easier to test.

Startup latency is paid everywhere#

Install speed gets the marketing screenshots, but process startup is just as important for small tools.

Many of my repositories contain short TypeScript scripts for release checks, metadata generation, deployment, synchronisation or smoke testing. Under a traditional Node setup, those scripts may run through tsx, ts-node, a precompile step or an esbuild wrapper.

With Bun, a script can usually execute directly:

{
  "scripts": {
    "sync": "bun src/sync.ts",
    "selftest": "bun src/selftest.ts",
    "deploy": "bun scripts/deploy.mjs"
  }
}

This is not mainly about deleting three lines from devDependencies. It removes another process and another layer of startup work from commands that may only run for a fraction of a second themselves.

That distinction becomes noticeable in chained scripts. A check such as:

bun run typecheck && bun run lint && bun run test && bun run build

starts several tools in sequence. Reducing the fixed overhead around each stage makes the complete validation loop feel more immediate even when TypeScript, ESLint, Vitest or esbuild still performs the substantive work.

Bun uses JavaScriptCore rather than V8. The engines have different strengths, and runtime performance depends heavily on the workload, but fast command startup has been one of Bun's design goals from the beginning. For CLIs, build scripts and automation, latency before useful work starts can matter more than peak throughput after a long warm-up.

The gains compound in CI and containers#

Local speed is useful. Repeated remote speed is where standardisation starts paying for itself.

My Bun repositories now follow a common pattern:

{
  "packageManager": "[email protected]"
}

and CI installs with either:

bun ci

or:

bun install --frozen-lockfile

Both enforce agreement between package.json and bun.lock. A job fails instead of quietly resolving a different dependency graph.

Pinning the package manager matters because “uses Bun” is otherwise underspecified. Lockfile behaviour, linker defaults and compatibility improve between releases. Declaring the version makes a repository's expected environment machine-readable for Corepack-aware tooling, contributors and coding agents.

The same approach applies to Docker. A frontend build can use the official Bun image, install from the frozen lockfile and run the existing Vite build without pretending that Vite has been replaced:

FROM oven/bun:1.3.14 AS build
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

Faster installation reduces build time when the dependency layer is invalidated. Faster startup reduces the overhead of script-heavy pipelines. A single runtime and package manager also reduces setup steps in GitHub Actions.

None of these is a spectacular optimisation alone. They are repeated optimisations, which is more valuable.

I am standardising the workflow, not replacing every tool#

A migration to Bun does not require turning every repository into a demonstration of every Bun feature.

Some of my projects still build with Vite, Astro, webpack or esbuild. Some still test with Vitest, Mocha, Playwright or Node's own test runner. One project deliberately retains a node --test invocation because the bundled test output targets node:test and already works correctly.

That is not an incomplete migration. Bun can provide the package manager and script runner while specialised tools continue doing their jobs.

I use a fairly conservative order:

  1. Add a pinned packageManager declaration.
  2. Generate and commit bun.lock.
  3. Remove competing npm, Yarn or pnpm lockfiles.
  4. Run existing scripts through bun run.
  5. Replace npx with bunx where behaviour matches.
  6. Run simple TypeScript and JavaScript utilities directly with Bun.
  7. Replace test or build tools only when there is a measurable reason.

This avoids the most common failure mode in toolchain migrations: changing package installation, runtime, tests and bundling simultaneously, then having no idea which change caused the regression.

Compatibility is good enough to be useful, not complete enough to ignore#

Bun has broad Node.js compatibility, but “drop-in replacement” should still be treated as a target rather than a guarantee for every package.

The areas I check most carefully are native modules, unusual Node APIs, subprocess behaviour and dependency lifecycle scripts. Bun does not execute arbitrary lifecycle scripts from installed dependencies by default. Packages that legitimately require them may need to be listed under trustedDependencies.

The security rationale is reasonable, but it can produce the confusing situation where installation succeeds and the package is not actually ready to run.

The test runner has a Jest-compatible API, but Bun's documentation still lists unsupported or incomplete Jest features. Existing Vitest or Node test suites therefore stay in place unless moving them provides a real benefit.

This is why the audit found mixed usage rather than 22 identical repositories. Some projects are Python, static data or infrastructure repositories. Others have Node workflows that have not yet justified a migration. GPT-Plus, for example, has no root JavaScript package setup but uses Bun 1.3.14 to run its scheduled upstream-check script in GitHub Actions.

The goal is a faster default, not a rule that every file must pass through Bun.

Anthropic buying Bun reduced one of the strategic risks#

Anthropic acquired Bun in December 2025. The financial terms were not disclosed.

The important part for users is the arrangement described by the Bun team: the project remains open source, MIT-licensed, developed in public and maintained by the same team. Anthropic is using Bun as infrastructure for Claude Code, the Claude Agent SDK and future coding products.

That creates unusually direct incentives. Claude Code is distributed as a Bun executable; regressions in Bun can become regressions in one of Anthropic's core developer products. Acquisition is never a guarantee of good stewardship, but this is a stronger sustainability story than a free runtime eventually needing to invent a cloud business around itself.

It also explains why performance remains central rather than becoming a side project. Anthropic benefits directly when Bun produces smaller executables, starts faster and behaves more predictably under agent-driven workloads.

The Rust rewrite makes the direction more interesting#

In July 2026, Bun announced that version 1.4 would be the first release written primarily in Rust rather than Zig. The current stable line I use is still Bun 1.3.14; the Rust version is available through the canary channel while 1.4 is prepared for release.

This was not a rewrite because Zig was considered slow. Bun credits Zig with making the original project possible. The issue was stability at the boundary between JavaScriptCore's garbage-collected objects and manually managed native memory. Recent fixes included use-after-free crashes, double frees and leaks across networking, compression, buffers and filesystem watchers.

Rust moves more of those lifetime rules into the type system. Drop, ownership and borrowing cannot prevent every bug because Bun still interfaces with C++, JavaScriptCore and other native libraries, but they can turn a class of cleanup mistakes into compiler feedback rather than production crashes.

The rewrite itself is technically notable. Jarred Sumner reports that one engineer used Claude Code workflows and a pre-release Claude model to mechanically port more than 535,000 lines of Zig, run adversarial reviews and get the complete cross-platform test suite passing in 11 days.

The early Bun 1.4 results reported by the team include:

  • 128 fixes for bugs reproducible in Bun 1.3.14
  • roughly 20% smaller Linux and Windows binaries
  • 2% to 5% improvements across published HTTP, Next.js, Vite and TypeScript benchmarks
  • stable memory use in a repeated Bun.build() test that previously leaked roughly 3 MB per build
  • a 10% reduction in Claude Code's median Linux startup time after it adopted the Rust port

These are again project-published numbers, and 1.4 canary is not the same thing as a mature stable release. Still, the rewrite addresses the main argument against adopting a younger runtime: whether its performance comes with too much instability.

A faster runtime is attractive. A faster runtime whose maintainers are investing heavily in memory safety and compatibility is easier to standardise on.

Performance was the reason; consistency makes it sustainable#

I did not move these projects to Bun because JavaScript needed another logo or because replacing npm is inherently interesting.

I moved them because fast installs, low startup overhead and direct TypeScript execution reduce the time between changing something and knowing whether it works.

The broader toolchain becomes simpler as a side effect. There is one expected package manager, one lockfile format, one pinned version and fewer single-purpose runtime dependencies. CI and local development use the same commands. Coding agents have fewer plausible ways to choose the wrong tool.

There will continue to be exceptions. Node remains the compatibility baseline for much of the ecosystem, and mature specialist tools should not be removed merely to produce a cleaner diagram.

But across 14 repositories, Bun has already become more than an experiment. It is the default layer underneath the frameworks and tools I was already using, and it makes that layer substantially faster.

Sources and further reading#