Moving this blog to Bun shortened startup and builds. The chart makes those gains visible, but the useful work was replacing file operations, updating CI and checking which Node assumptions still held.
Part 1 covers why I made the switch. This is the migration itself.
Start with the Smallest Change
I started with the scripts in package.json:
"scripts": {
- "start": "ts-node src/index.ts",
- "build": "tsc",
- "dev": "nodemon --watch src --exec ts-node src/index.ts",
+ "start": "bun src/index.ts",
+ "build": "bun build ./src/index.ts --outdir ./dist --target=bun",
+ "dev": "bun --watch src/index.ts",
}
Bun runs TypeScript directly, so I could remove the separate compile step. Its bundler also replaced the build configuration I had accumulated around TypeScript.
Then I moved CI:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.11
- run: bun install
- run: bun run build
- run: bun run typecheck
That replaced a 4,500-line package-lock.json with bun.lock.
The File APIs Were the Real Migration
The biggest payoff came from converting Node’s fs operations to Bun’s native File API.
I used Claude Code to find the file-system calls and review them one at a time:
- Scan for patterns like
fs.readFileandfs.pathExistsacross the codebase - Group the results by how each file was used
- Compare the Node and Bun versions side by side
- Apply and test each change separately
It also suggested arrayBuffer() for binary operations, which I probably wouldn’t have reached for on my own.
What Didn’t Transfer Cleanly
Not every Node package worked with Bun. Documentation was thinner, and Bun’s errors sometimes gave me less to work with than Node’s. Native C and C++ extensions were the biggest compatibility risk.
Test the dependencies that matter Don’t assume Node compatibility means your production path works. Check native extensions and the code behind your critical requests before changing runtimes.
The Result
The percentage drop alone makes startup look like the larger improvement. Figure 1 puts both steps on the same time axis to show how much waiting each change removed.
| Stage | Node.js (s) | Bun (s) | Change |
|---|---|---|---|
| Startup | 1.2 | 0.3 | -75.0%0.9 s saved |
| Build | 37.0 | 20.0 | -45.9%17 s saved |
The chart and table show the comparison without hovering. When available, tap or use arrow keys for interactive inspection.
The 2025 article's recorded local build-tool observations, not serverless request timings or a current runtime benchmark. Hardware, repetitions and distributions were not published. No new measurements were run for this chart.
The build step saved more seconds per execution, even though startup improved more in percentage terms. The dependency count also fell from 20+ to 15. I ended up with fewer configuration files and more direct file-system code. That matters more to me than winning a runtime benchmark.
How I Would Do It Again
I wouldn’t migrate everything in one pass. The order that worked was:
- Package management and simple scripts
- Build configuration
- CI
- File operations
- Dependency cleanup
That order kept each failure small enough to understand. For this I/O-heavy codebase, the migration paid for itself within the first week. I didn’t need to keep Node just because the project started there.
Was this useful?
Thanks for the feedback.