Introduction
Changing a development tool is rarely just a matter of replacing a few commands in package.json.
For a production web project with CI/CD, the application code, dependency lockfile, automation scripts, GitHub Actions, and Pages deployment environment all need to agree on the same runtime contract.
I migrated the actively maintained dasomel.github.io repository from a Node.js + npm-centered toolchain toward Bun 1.4.
The final direction looks simple:
npm
↓
Bun 1.4The actual migration was not quite that simple.
The first attempt failed because the build scripts had started calling Bun while the GitHub Pages deployment workflow did not provision Bun. After fixing that gap, I completed the dependency and automation migration, added Bun-native validation, measured actual GitHub Actions performance, and finally removed duplicate Next.js production builds between PR validation and Pages deployment.
This post is therefore not a generic “Bun is fast” article. It is an engineering record of failure, diagnosis, repair, measurement, and pipeline optimization.
1. The original state
The project originally used Node.js and npm as its primary tooling.
A representative build script looked like this:
{
"scripts": {
"build": "next build && node scripts/generate-rss.js"
}
}GitHub Actions installed dependencies with npm:
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci --prefer-offline --no-audit --no-fundThe repository also had multiple Node.js automation scripts for RSS generation, daily digest processing, event collection, and project metadata refresh.
The effective model was:
Next.js
└─ Node.js
automation scripts
└─ Node.js
package install
└─ npm
CI / Deploy
└─ Node + npm2. Why move to Bun?
The motivation was not simply to find a faster package manager.
The repository contains enough JavaScript automation that a consistent runtime and package toolchain is more valuable than having several partially overlapping execution models.
The target model was:
Next.js
└─ executed through Bun
automation scripts
└─ Bun
package install
└─ bun install
lockfile
└─ bun.lock
CI / Deploy
└─ Bun 1.4This does not mean replacing Next.js's internal compiler or Turbopack with Bun. Next.js remains the application framework and build engine; Bun becomes the surrounding package/runtime/tooling layer.
3. The first migration failed
The first step was to move automation scripts to Bun.
For example:
- "build": "next build && node scripts/generate-rss.js"
+ "build": "next build && bun scripts/generate-rss.js"Other event, digest, and metadata commands were updated the same way.
The project also started declaring the required Bun version:
"engines": {
"bun": ">=1.4.0"
}But this exposed a deployment gap.
The Pages workflow still did not have Bun
The build script now expected:
next build
↓
bun scripts/generate-rss.jsThe Pages runner, however, did not yet install Bun.
The real failure path was:
Next.js build
↓
success
↓
RSS generation through Bun
↓
Bun missing on Pages runner
↓
bun: not foundThe important point is that Next.js itself was not the failing component. The source code had acquired a new runtime requirement, but the deployment environment had not been updated to provide it.
The fix was to provision Bun explicitly in the Pages workflow:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0
- name: Verify Bun version
run: bun --versionThe repair landed in commit a3388fa9.
4. The first lesson: runtime is part of the change
This looked like a small CI configuration issue, but it exposed a broader principle:
The runtime required by the source code and the runtime provisioned by the execution environment should be treated as one change unit.
A local success such as:
bun run buildis not enough.
You must also validate:
Local
CI validation
Automation workflows
Pages deploymentThe first failure happened precisely because one of those execution boundaries was left behind.
5. Second stage: complete the Bun-native toolchain
After repairing Pages, I completed the repository-wide tooling transition.
Dependency state
package-lock.json
↓
bun.lockAutomation scripts
node scripts/xxx.mjs
↓
bun scripts/xxx.mjsBuild
next build && node scripts/generate-rss.js
↓
next build && bun scripts/generate-rss.jsCI runtime
Bun 1.4 became an explicit CI dependency and the version was verified on every run.
I also added Bun-native validation:
bun test
bun run build:tools
bun run buildBun was no longer just an additional runtime. It became the repository's package manager, automation runtime, test runner, and build tooling.
6. Measuring the real CI speed
Instead of assuming Bun was faster, I compared actual GitHub Actions logs from the same GitHub-hosted ubuntu-24.04 runner.
Dependency installation
The npm-based run took about 15.26 seconds.
The Bun-based run reported:
bun install --frozen-lockfile --no-progress
694 packages installed [8.99s]So the observed dependency-install result was:
npm ci 15.26s
bun install 8.99s
-------------------
Difference 6.27s
Improvement ~41%This was the clearest direct performance improvement from the migration.
7. Next.js itself did not suddenly get much faster
One of the most useful findings was that the Next.js build time stayed almost the same.
The previous build compiled Next.js in roughly 10.3 seconds.
The Bun-based build compiled it in roughly 9.8 seconds.
The overall production build remained in the low-20-second range.
In other words:
Bun migration
↓
dependency installation ✅ significant improvement
Next.js/Turbopack build ≈ nearly unchangedThat makes sense because this migration did not replace Next.js's internal compiler.
So the credible performance claim for this repository is not “Bun makes Next.js dramatically faster.” It is:
Bun reduced the repeated dependency and tooling overhead around the build.
8. The full CI job became shorter
The end-to-end workflow comparison was more interesting.
Earlier workflow
checkout
Node setup
npm cache restore
npm ci
next build
RSS generation
cleanupTotal runtime was about 49.5 seconds.
Bun migration workflow
checkout
Bun setup
Bun cache restore
bun install
bun test
bun build smoke test
next build
RSS generation
cleanupTotal runtime was about 38.5 seconds.
The observed change was:
Before ≈49.5s
After ≈38.5s
-------------------
~11s shorter
~22% fasterAnd the newer workflow actually contains more validation steps:
bun test
bun run build:toolsSo the improvement was not simply the result of deleting checks.
9. One more optimization: remove duplicate production builds
After the Bun migration was stable, I reviewed the CI structure again.
The PR validation workflow was still running the full production build:
PR CI
└─ bun run build
↓
Next.js production buildAfter merge, the Pages deployment ran the same production build again:
main push
└─ bun run build
↓
Next.js production buildThat meant the same change could trigger two full Next.js production builds before the site was actually published.
The roles were split instead.
PR CI — fast quality validation
PR CI now runs:
bun install --frozen-lockfile
↓
bun test
↓
bun build smoke test
↓
bun lintThe question is:
“Is this change safe to merge?”
Pages Deploy — the production build boundary
Once merged to main, Pages runs:
bun install --frozen-lockfile
↓
bun test
↓
bun lint
↓
bun run build
↓
GitHub Pages deployThe question is:
“Can this exact main revision be built and deployed as production?”
This removes the redundant PR production build while keeping the final production build mandatory immediately before deployment.
The revised PR CI was verified with successful bun test, Bun bundle smoke test, and lint runs, with the full Next.js production build removed from the PR path.
10. The measured result
The main measurements from the actual Actions logs are summarized below.
| Metric | npm-based | Bun-based |
|---|---|---|
| Dependency install | 15.26s | 8.99s |
| Next.js compile | 10.3s | 9.8s |
| Overall build flow | ~23s | ~24s |
| Whole CI job | ~49.5s | ~38.5s |
| Unit/runtime test | not present | bun test |
| Tool bundle smoke test | not present | bun build |
| PR production build | yes | removed |
| Main production build | yes | yes |
So the most accurate one-line conclusion is:
The migration did not dramatically accelerate Next.js itself, but it reduced the total CI cycle by about 22% while adding Bun-native validation and then removed a second, redundant production build from the PR path.
11. Post-migration cleanup matters too
One more small issue appeared during the migration: the Pages workflow had accumulated duplicate Bun setup/verification blocks.
Conceptually it looked like:
Setup Bun
Verify Bun
...
Setup Bun
Verify BunIt worked, but it was unnecessary duplication.
The final cleanup kept a single Bun 1.4 setup and verification block in the Pages deployment workflow.
This is an easy detail to overlook. A migration is not finished when the pipeline turns green; it is finished when the resulting configuration is also simple and maintainable.
12. Engineering lessons
Runtime migration is bigger than package.json
package.json change
≠
complete migrationCI, Pages, automation workflows, caches, and the lockfile all belong to the same change boundary.
Performance should be measured in the actual repository workflow
A generic benchmark is useful, but the more relevant numbers were those observed in the project itself:
15.26s → 8.99sfor dependency installation, and:
≈49.5s → ≈38.5sfor the complete initial migration CI.
A component not getting faster is still a useful result
Next.js did not become dramatically faster. That is fine.
It lets us make a precise statement about where Bun helped instead of attributing every improvement to Bun.
PR and Deploy do not need identical responsibilities
PR validation should answer whether a change is safe to merge. The deployment workflow should own the definitive production build.
Failure history is engineering evidence
The first failed deployment was not noise to hide. It identified the runtime provisioning gap and led directly to a more explicit and robust deployment configuration.
Conclusion
The Bun migration did not succeed on the first attempt.
The first attempt changed the build scripts to call Bun, but the Pages deployment environment did not have Bun installed:
build script
↓
Bun call
↓
Pages runner
↓
Bun missing
↓
failureThat failure exposed the missing runtime provisioning step. After fixing it, I completed the dependency and automation migration, added runtime and smoke validation, and measured the result in real GitHub Actions runs.
The observed results were:
Dependency install
npm ci 15.26s
bun install 8.99s
↓
~41% improvement
Whole initial CI
≈49.5s
↓
≈38.5s
↓
~22% improvementAt the same time, the Next.js build itself stayed almost unchanged.
The final pipeline was then simplified further by removing the redundant PR production build and keeping the definitive Next.js build only at the Pages deployment boundary.
So the most accurate conclusion is:
Bun did not magically make Next.js fast. It simplified and accelerated the dependency/tooling layer of the repository, reduced the real CI cycle by about 22%, and allowed the PR pipeline to become faster by removing a redundant production build.
For me, the more important outcome was methodological: fail, isolate the cause, repair the execution boundary, measure again, and then simplify the pipeline.
That is a much more useful way to approach a tooling migration than simply declaring that the new tool is faster.
12. Sequential main changes and cancelled deploy runs are part of the deployment model
While cleaning up the Bun-based CI/CD flow, I observed a case where a newer main revision arrived while an earlier GitHub Pages deployment was still running. The earlier run became cancelled, and a new deployment started for the latest revision.
A cancelled run does not mean the earlier code was deleted. In a linear main history, the newer revision normally contains the previous revision's changes, so the earlier deployment work can be stopped and the latest revision can become the final deployment target.
When diagnosing a deployment, check all three signals together: the previous run's cancelled state, the presence of a newer main revision, and the outcome of the latest deployment.
In this case, Deploy #364 was cancelled after a newer commit appeared, and Deploy #365 started for 76e1a3e; its production build completed successfully.
This is better described as converging deployment state on the latest main revision than as blindly overwriting a previous result. Systems publishing independent artifacts may need a different concurrency policy.