How It Works
Pipeline
Section titled “Pipeline”next buildruns your normal Next.js build. BecauseadapterPathpoints at next-bun-compile, Next invokes the adapter’sonBuildCompletewith its typed build outputs: every route’s traced assets, prerender metadata (revalidate times, PPR state, headers), middleware matchers, and routing rules.- The adapter assembles a standalone-equivalent tree from those
traced outputs — per-route assets, the server runtime’s NFT trace,
prerender seed files.
output: "standalone"is never used. - Code generation scans the assembled tree, discovers
externalized packages and turbopack-mangled aliases, computes which
pages can be served frozen from memory, and generates
server-entry.js+assets.generated.js. - Bun compiles the entry, the serve runtime, and every embedded asset file into one minified binary. Next.js itself is not bundled — it loads at runtime from the extracted tree, so nothing is carried twice.
The three-tier runtime
Section titled “The three-tier runtime”A single Bun.serve owns the port. Requests route through the
fastest tier that can answer them correctly:
- Tier 1 — static assets.
/_next/static/*andpublic/*served from memory with precompressed gzip, ETags, and immutable cache headers. - Tier 2 — frozen prerendered pages. Pages with
revalidate: false, no PPR postponed state, and no covering middleware or routing rule are served from memory with full RSC content negotiation. Non-GET methods (server actions!) always fall through to Next. - Tier 3 — everything else. PPR resume, ISR, API routes, server
actions, and dynamic rendering go to Next’s own
router-serverrequest handler in-process, through a fetch→node bridge — the exact stacknext startruns, minus its HTTP listener. ISR and cache-component responses additionally get an in-memory L1 cache.
Revalidation stays Next’s job. The runtime patches the default
filesystem cache handler so every revalidateTag/revalidatePath
and background regeneration drops the affected page from the memory
tiers — semantics are identical to next start, just faster between
changes. With a custom cacheHandler configured, the page tiers turn
themselves off entirely.
Next boots lazily on the first dynamic request; the static tiers answer from the first millisecond the port is open.
What gets embedded
Section titled “What gets embedded”- All static files in
.next/static/(skipped ifassetPrefixset) - All public files
- Prerendered pages: HTML, RSC payloads, and their metadata
- The
.next/server/tree (chunks, manifests, build ID) - Every externalized server package — traced from your
node_modules/into the binary as file assets - The server orchestration graph (router-server chain), traced with
the same pruning
output: "standalone"applied
Runtime: asset extraction
Section titled “Runtime: asset extraction”On first run, the binary writes its embedded assets to disk relative
to process.execPath — or to NBC_RUNTIME_DIR if set (point it at
tmpfs for RAM-backed runtime files and read-only root filesystems).
It then stamps .next/.nbc-extracted with a content hash of the
build plus the directory it extracted into. Later boots compare that
stamp: a match skips extraction entirely (one file read), while a
mismatch — new binary, moved directory, or a half-finished previous
extraction — re-extracts everything with overwrite so nothing stale
can shadow the embedded assets. Extraction is bounded to 64
concurrent writes so it can’t exhaust file-descriptor limits.
Resolver hook for externalized packages
Section titled “Resolver hook for externalized packages”Next.js with Turbopack externalizes some packages (sharp, bcrypt,
anything in serverExternalPackages, anything with native bindings) —
they’re loaded at runtime instead of bundled. Two things make that
challenging in a compiled binary:
- Mangled names. Turbopack rewrites
require("sharp")torequire("sharp-457ea9eae1af1a9c")in chunks, with a build-time symlink at.next/node_modules/sharp-457... → sharp. The compiled binary has no node_modules tree pre-baked. - Bun’s compiled-binary resolver quirk. From inside an extracted
node_modulesentry (e.g.sharp/lib/sharp.js), bun’s resolver in compiled-binary mode doesn’t reliably walk up to find sibling packages.
Two mechanisms
Section titled “Two mechanisms”1. Build-time chunk rewrite
Section titled “1. Build-time chunk rewrite”next-bun-compile scans server chunks for "<name>-<16 hex>" string
literals (the mangled-alias pattern) and replaces each with the
absolute file path of the canonical target:
require("sharp-457ea9eae1af1a9c")require("__NBC_BASE__/.next/node_modules/sharp/lib/index.js")__NBC_BASE__ is a placeholder substituted with the real baseDir
when extractAssets writes each chunk to disk. Resolution becomes a
direct file load — no walks, no exports-map ambiguity, no resolver
involvement. Works for both CJS require() and ESM import().
For subpath imports (import("prettier-.../plugins/html")),
next-bun-compile reads each canonical’s package.json + exports map
to pick the right .mjs/.js/.cjs variant, then rewrites to that
absolute path.
2. Runtime Module._resolveFilename hook
Section titled “2. Runtime Module._resolveFilename hook”Once execution is inside an extracted package (sharp’s own
require('detect-libc'), dynamic interpolated requires like
require('@img/sharp-PLATFORM/sharp.node')), the chunk rewrite no
longer applies. A Module._resolveFilename hook catches CJS resolves
that bun’s compiled-binary resolver fails on and reimplements
Node-compatible resolution from scratch:
- Walk node_modules from the calling file
- Read
package.json+mainfor top-level - Honor
exportsmaps (require/node/defaultconditions) - Handle directory subpaths with their own
package.json+main(e.g.next/dist/compiled/source-map)
The hook also consults an alias map (sharp-457... → sharp) and
redirects mangled names that slip past the build-time rewrite.
Module stubs (dev-only code paths)
Section titled “Module stubs (dev-only code paths)”Some modules can’t be resolved at compile time but are never reached
in production — Next’s dev server, the dev bundler, optional deps
loaded in try/catch. next-bun-compile creates no-op stubs for these
only if the real module isn’t installed. If you do install
@opentelemetry/api or critters, the real package gets bundled
instead.
Pre-flight validator (build time)
Section titled “Pre-flight validator (build time)”For every alias + subpath the chunks reference, next-bun-compile checks whether resolution found a file. If anything didn’t resolve, it warns at build time:
next-bun-compile: ⚠ 1 of 4 turbopack alias reference(s) won't resolve at runtime: ✗ some-pkg-deadbeef/foo → some-pkg/fooSet NEXT_BUN_COMPILE_VERBOSE=1 during build to see every
resolution + the file it picked.
Debug mode (runtime)
Section titled “Debug mode (runtime)”Set NEXT_BUN_COMPILE_DEBUG=1 when running the binary to log every
resolver-hook decision: alias redirects, fallback walks, fallback
failures, with parent-file context. See
Debug mode.