iTranslated by AI
Exploring how Next.js runs with bun dev
Bun's runtime includes a mode for launching a Next.js server. I was curious about what this actually does, so I observed the behavior and examined the source code.
-
bun devserves as the command to start the HTTP server. - This is likely because Bun was originally created with the goal of speeding up dev-servers.
- Whether to use Next is specified in the
bunfig.tomlconfiguration file, but it can also be explicitly stated withbun --use next.
framework = "next"
- Running
bun bun --use nextgenerates two binaries:./node_modules.bunand./node_modules.server.bun. -
bun devexecutes these.bunfiles. They can also be specified via--bunfileand--server-bunfile. - If
bun devis run without options and not specified in the configuration file, routes will not be resolved, resulting in a 404 error. - Therefore, let's look at what processing is added when this flag is enabled.
dev_command.zig
const Server = @import("../http.zig").Server;
pub const DevCommand = struct {
pub fn exec(ctx: Command.Context) !void {
Global.configureAllocator(.{ .long_running = true });
try Server.start(ctx.allocator, ctx.args, @TypeOf(ctx.debug), ctx.debug);
}
};
-
Global.configureAllocatorseems a bit like magic, and its exact function is unclear. -
global.zigand__global.zigare defined, suggesting the implementation might be swapped somewhere. - The Server implementation is in
http.zig, so let's look at that.
http.zig
- Requests are processed using the
Serverstruct and aHandlerfunction in the HTTP server.
Entry point: Server.start()
pub fn start(allocator: std.mem.Allocator, options: Api.TransformOptions, comptime DebugType: type, debug: DebugType) !void
- The second argument
optionsincludes thenextflag.- The process of how it is transformed into
Api.TransformOptionsis unclear.
- The process of how it is transformed into
- Allocating the
Server. - Attaching the
Bundleto the server. - Calling
configureLinker()andconfigureRouter()of theBundler. - Since
optionsis used here, this is the part where it integrates with the application.
server.bundler = try allocator.create(Bundler);
server.bundler.* = try Bundler.init(allocator, &server.log, options, null, null);
server.bundler.configureLinker();
try server.bundler.configureRouter(true);
bundler.zig
-
configureLinker()is a function that initializes theLinkerfield. -
configureRouter()is a function that defines the behavior of theLinkerfield based on the information inoptions.routes. - When
Bundler.init()is executed,Options.fromApi()is called, and the framework configuration information is included in its arguments.
bun-framework-next
- An npm package included in the repository.
- Since it is added to the
devDependenciesof applications created with Bun, this module acts as an intermediary for theServer. - It doesn't do anything particularly special; it serves as glue code to handle Next.js application requests using Bun.serve.
- As an experiment, I tried deleting this module after generating the binary with
bun bun, and the routes stopped working, confirming that it is loaded at runtime.
node_modules.bun, node_modules.server.bun
❯ file node_modules.server.bun
node_modules.server.bun: sticky a /usr/bin/env bun script executable (binary data)
I didn't quite understand what these binaries were at first.
Since they were executables, I initially thought the bundled result had been converted into native code, but looking inside, that wasn't the case.
#!/usr/bin/env bun
o<cd>
^@
var BUN_RUNTIME=(()=>{var fr=Object.create;var G=Object.defineProperty;var cr=Object.getOwnPropertyDescriptor;var sr=Object.getOwnPropertyNames;var ...
By running bun node_modules.bun, you can extract the binary as the JavaScript code shown above.
This file contains references to paths under node_modules/, which explains why it stops working if you delete that directory.
Hot Module Replacement
Regarding how Bun implements the HMR mechanism of next-dev-server, the following code, which runs in the browser, was executed at bundle time.
fixed_buffer_writer.print(
\\if ('window' in globalThis) {{
\\ (function() {{
\\ BUN_RUNTIME.__injectFastRefresh(${x}());
\\ }})();
\\}}
The entity of __injectFastRefresh() is a function implemented in TS using react-refresh (it is added to the application's devDependencies).
function injectFastRefresh(RefreshRuntime) {
if (!FastRefreshLoader.hasInjectedFastRefresh) {
RefreshRuntime.injectIntoGlobalHook(globalThis);
FastRefreshLoader.hasInjectedFastRefresh = true;
}
}
What I Discovered
- An HTTP server written in Zig is built-in, and JS dependency modules are bundled into a proprietary binary and loaded at startup. This is likely faster than a server started with Node, which processes JS for each request (Hypothesis).
- Processes that would typically be handled in the core of Next.js, such as bundling, refresh, and request handling, were also included in Bun.
- This suggests that
bun devis intended to serve as a high-speed interface fornext dev.
- This suggests that
- However, since the JS under
node_modules/is still necessary forbun devto function, not everything in the application is converted to a binary.- This is because
bun-framework-nextitself runs in the JSC context and references the Next.js rendering code from within. - It seems the proprietary binary and
node_modules/work together.
- This is because
-
bun-framework-nextrunning on the Bun HTTP server returns responses via the Next.js interface. - The rendering utilizes the code from the Next.js core npm package.
- Bun's proprietary binary bundle and
node_modules/are referenced at runtime. - For client-side Hot Reloading, Bun injects the
react-refreshJS code into the bundle.
Discussion