karawaci.kode

2026-06-28 · 7 min

Monorepo: Turborepo vs Bun Workspaces 8 Bulan

Delapan bulan lalu, monorepo SaaS klien Karawaci punya 14 package (5 app Next.js, 3 service Bun, 4 shared library, 2 internal CLI). Kami pakai Turborepo + npm workspaces. Cold install 4 menit 20 detik di CI, dan tim 6 engineer mulai mengeluh tiap branch baru.

Hari ini, kami pakai Bun Workspaces (bun install + custom orchestration). Saya share angka, dan kenapa saya tetap pertahankan Turborepo untuk satu use case.

Setup awal

  • Repo: 14 package, 78k LOC TypeScript, 12k LOC TSX.
  • Dependencies total: 1.247 di lockfile (deduped), ~287 unique top-level.
  • CI: GitHub Actions, runner ubuntu-latest 4 vCPU 16GB.
  • Tim: 6 engineer (3 Tangerang, 2 Jakarta, 1 Bandung).
  • Deploy cadence: 5-8 deploy/hari ke staging, 2-4 ke production.

Baseline Turborepo + npm:

  • Cold install (no cache): 4 menit 20 detik
  • Warm install (lockfile sama): 1 menit 18 detik
  • Full build (turbo run build, no cache): 6 menit 45 detik
  • Full build (turbo cache hit): 22 detik
  • Remote cache hit ratio: 71%

Kenapa migrate

Tiga keluhan tim:

  1. Cold install lambat: branch baru = 4+ menit nunggu CI. Untuk PR review-driven workflow yang kami pakai (push → CI run → review), ini bottleneck.
  2. node_modules size: 2.8GB. Disk pressure di laptop dev MacBook M2 256GB.
  3. TypeScript transpilation di shared lib: tsc watch mode lambat, ~12 detik per change di shared package.

Saya sudah jalan dengan Bun di payment gateway production selama 4 bulan, jadi confidence-nya ada.

Migrasi: 3 hari, 1 weekend, 4 PR

Hari 1: ganti package manager

// package.json root
{
  "workspaces": ["apps/*", "services/*", "packages/*", "tools/*"],
  "packageManager": "bun@1.3.12"
}

Hapus package-lock.json, jalankan bun install.

Cold install: 38 detik. Dari 4m20s. Sekitar 87% reduction.

Sekitar 80% dari speed-up itu adalah Bun’s parallel network fetch + native install (vs npm yang single-threaded di banyak phase). Sisa 20% dari Bun’s lockfile format (binary, faster to parse).

Hari 2: orchestration replacement

Turborepo kasih kami:

  • Task pipeline (build depends on ^build)
  • Remote cache (kami pakai Vercel remote cache, $20/mo)
  • Filtered task runs (turbo run test --filter=...[origin/main])

Bun Workspaces tidak punya equivalent built-in. Saya tulis scripts/orchestrate.ts:

import { execSync } from 'node:child_process';
import { readdirSync, readFileSync } from 'node:fs';

interface Workspace {
  name: string;
  path: string;
  dependencies: string[];
}

function loadWorkspaces(): Workspace[] {
  const root = JSON.parse(readFileSync('package.json', 'utf-8'));
  const patterns = root.workspaces as string[];
  // ... glob expand, read each package.json
}

function topologicalSort(workspaces: Workspace[]): Workspace[] {
  // Kahn's algorithm, dependency order
}

function runTask(task: string, affected: string[]) {
  const sorted = topologicalSort(loadWorkspaces())
    .filter(w => affected.includes(w.name));
  
  for (const ws of sorted) {
    console.log(`[${ws.name}] ${task}`);
    execSync(`bun run ${task}`, { cwd: ws.path, stdio: 'inherit' });
  }
}

180 LOC, no external deps. Tidak punya remote cache, tapi punya local content-hash cache yang saya simpan di .bun-cache/.

Hari 3: CI integration

GitHub Actions config:

- uses: oven-sh/setup-bun@v2
  with:
    bun-version: 1.3.12

- uses: actions/cache@v4
  with:
    path: |
      ~/.bun/install/cache
      .bun-cache
    key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
    restore-keys: bun-${{ runner.os }}-

- run: bun install --frozen-lockfile
- run: bun run scripts/orchestrate.ts build --affected

--affected detect changed packages via git diff vs main, build hanya yang impacted plus dependents.

Hasil 8 bulan

MetricTurborepo + npmBun WorkspacesDelta
Cold install CI4m 20s38s-85%
Warm install CI1m 18s6s-92%
Full build (no cache)6m 45s5m 12s-23%
Affected build (avg)1m 8s42s-38%
Cache hit ratio71% (remote)88% (local)+24%
node_modules size2.8 GB1.4 GB-50%
CI cost (GitHub minutes)$87/mo$34/mo-61%

Cache hit ratio naik karena content-hashing saya per-package lebih granular dari Turborepo default. Turborepo hash include semua deps transitif yang sometimes overinvalidate.

Yang break

1. sharp native module

Image processing pakai sharp. Bun install download prebuilt binary tapi binary itu Node-ABI specific. Crash di runtime:

Error: Could not load the "sharp" module using the linux-x64 runtime

Fix: rebuild eksplisit per workspace yang pakai sharp:

{
  "scripts": {
    "postinstall": "bun pm trust --all"
  }
}

Plus pin sharp ke versi yang punya Bun-compatible prebuilt (v0.34+).

2. TypeScript path mapping di Vite

Vite 5 kami pakai untuk salah satu app. tsconfig.json paths:

{
  "paths": {
    "@kami/ui": ["../packages/ui/src/index.ts"]
  }
}

Bun resolve correctly, tapi Vite (via vite-tsconfig-paths plugin) kadang miss-resolve karena symlink structure Bun beda. Symptom: dev server kasih Failed to resolve import random.

Fix: explicit alias di vite.config.ts:

resolve: {
  alias: {
    '@kami/ui': fileURLToPath(new URL('../../packages/ui/src/index.ts', import.meta.url))
  }
}

Ugly, tapi works.

3. peerDependencies warning hell

Turborepo lebih permissive untuk peer dep mismatch. Bun strict, kasih warning verbose di tiap install. Saya silence di CI dengan --silent flag dan log warning ke artifact untuk audit weekly.

4. No remote cache

Local cache saja tidak cukup saat 6 engineer parallel push branch. Engineer A bangun shared lib version X, engineer B push 2 menit kemudian dengan dep yang sama — B rebuild dari nol. Turborepo remote cache mengatasi ini.

Workaround saya: self-host Turborepo remote cache server (open source: turbo-remote-cache) di VPS Hetzner kecil (€4/mo). Saya kombinasikan: Bun untuk install, Turborepo cache server untuk build artifact sharing.

Yes, saya combine keduanya. Tidak ideal, tapi practical.

Kapan saya tetap rekomendasi Turborepo

  • Tim > 10 engineer dengan high branch concurrency
  • Butuh visualisasi task graph (Turborepo punya --graph yang output Mermaid)
  • Heavy reliance di remote cache dari Vercel (jika sudah subscribe Vercel Pro)
  • Mixed package manager (Turbo support pnpm, yarn, npm sekaligus)

Kapan Bun Workspaces cukup

  • Tim < 8 engineer
  • Monorepo < 20 package
  • Mayoritas TypeScript / JavaScript (tanpa heavy native module)
  • Cost-conscious (CI minute matters)

Verdict

Bun Workspaces untuk monorepo SaaS Indonesia ukuran SMB-mid: pilihan yang menghemat CI time dan disk. Untuk monorepo ukuran enterprise dengan tim 15+ engineer dan remote cache mandatory: Turborepo masih unggul.

Saya tidak akan migrate lagi seandainya monorepo ini punya 5 engineer atau kurang — overhead 3 hari migration tidak worth untuk team kecil yang sudah comfortable. Tapi untuk tim 6 yang complain tiap minggu: ROI tercapai dalam 3 minggu (CI cost saved alone bayar engineering time).

Untuk konteks deployment, lihat juga catatan saya di Cloudflare Pages vs Vercel.

Ditulis oleh Reza Pradipta