KahWee - Web Development, AI Tools & Tech Trends

Expert takes on AI tools like Claude and Sora, modern web development with React and Vite, and tech trends. By KahWee.

Switching from Node to Bun - Part 1

Build times dropped from 37 seconds to 20 seconds — a 46% improvement — after I switched this blog's codebase from Node.js to Bun. The code got cleaner too. This is Part 1 covering why I switched and the key improvements. Part 2 covers the migration process.

Why Bun?

Native speed: Built on JavaScriptCore (WebKit's engine) instead of V8.

Integrated tooling: Package manager, bundler, test runner in one binary.

TypeScript support: First-class, no extra dependencies.

Modern APIs: Native File and Glob implementations that outperform Node alternatives.

TOML support: Built-in parser for configuration files.

File Operations: Before and After

The biggest wins came from replacing Node's file system operations with Bun's native File API.

Before (Node.js with fs-extra):

// Check if file paths exist
if (await fs.pathExists(withSlash)) {
  filePath = withSlash;
} else if (await fs.pathExists(withoutSlash)) {
  filePath = withoutSlash;
}

// Read file content
const content = await fs.readFile(filePath);
let htmlContent = content.toString();

After (Bun's File API):

// Check if file paths exist
if (await Bun.file(withSlash).exists()) {
  filePath = withSlash;
} else if (await Bun.file(withoutSlash).exists()) {
  filePath = withoutSlash;
}

// Read file content
const bunFile = Bun.file(filePath);
let htmlContent = await bunFile.text();

The Bun version runs faster. The native File API handles existence checks and reads with less overhead than Node's implementation.

Bun's Native Glob API

I replaced recursive directory traversal with Bun's native Glob API.

Before (Node.js recursion):

async function getMarkdownFilesRecursively(dir: string): Promise<string[]> {
  const entries = await fs.readdir(dir, { withFileTypes: true });

  const files = await Promise.all(
    entries.map(async (entry) => {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        return getMarkdownFilesRecursively(fullPath);
      } else if (entry.isFile() && entry.name.endsWith(".md")) {
        return [fullPath];
      }
      return [];
    }),
  );

  return files.flat();
}

After (Bun's Glob API):

async function getMarkdownFilesRecursively(dir: string): Promise<string[]> {
  const glob = new Bun.Glob("**/*.md");
  const files: string[] = [];

  for await (const file of glob.scan({
    cwd: dir,
    absolute: true,
  })) {
    files.push(file);
  }

  return files;
}

Tip

The Bun implementation is simpler and faster — fewer lines, fewer edge cases, easier to maintain.

Why Bun Is Fast

JavaScriptCore engine: Apple's JSC engine outperforms V8 for many operations.

Native implementations: File, HTTP, and core APIs are written in Zig, not JavaScript. No abstraction layers.

Optimized I/O: Bun's file operations bypass Node's libuv layer.

Single binary: Runtime, bundler, and package manager share one process. No inter-process communication overhead.

Built-in TypeScript: No separate compilation step.

Zero-copy architecture: Minimizes memory copies, especially for file operations.

These architectural choices compound. I/O-intensive tasks like static site generation benefit the most.

Continue to Part 2 for the migration process, challenges, and results.