Bun trong Thực Tế: Điều Gì Hoạt Động, Điều Gì Không, và Điều Gì Khiến Tôi Bất Ngờ
Bun với vai trò runtime, trình quản lý gói, bundler và test runner. Benchmark thực tế, những khoảng trống tương thích với Node.js, mô hình migration, và nơi tôi sử dụng Bun trong production ngày nay.
Cứ vài năm một lần, hệ sinh thái JavaScript lại có một runtime mới và các cuộc thảo luận diễn ra theo một kịch bản quen thuộc. Hype. Benchmark. "X đã chết." Kiểm chứng thực tế. Và rồi ổn định vào những trường hợp sử dụng mà công cụ mới thực sự tỏa sáng.
Bun đang ở giữa vòng cung đó ngay bây giờ. Và không giống hầu hết các đối thủ, nó đang trụ vững. Không phải vì nó "nhanh hơn" (mặc dù thường là vậy), mà vì nó đang giải quyết một vấn đề thực sự khác biệt: chuỗi công cụ JavaScript có quá nhiều thành phần chuyển động, và Bun gộp tất cả lại thành một.
Tôi đã sử dụng Bun ở nhiều khía cạnh hơn một năm nay. Một phần trong production. Một phần thay thế các công cụ mà tôi nghĩ mình sẽ không bao giờ thay thế. Bài viết này là một đánh giá trung thực về điều gì hoạt động, điều gì không, và những khoảng trống nào vẫn còn quan trọng.
Bun Thực Sự Là Gì#
Hiểu lầm đầu tiên cần làm rõ: Bun không phải là "một Node.js nhanh hơn." Cách đặt vấn đề đó đánh giá thấp nó.
Bun là bốn công cụ trong một file nhị phân:
- Một runtime JavaScript/TypeScript — chạy code của bạn, giống Node.js hoặc Deno
- Một trình quản lý gói — thay thế npm, yarn, hoặc pnpm
- Một bundler — thay thế esbuild, webpack, hoặc Rollup cho một số trường hợp sử dụng
- Một test runner — thay thế Jest hoặc Vitest cho hầu hết các bộ test
Sự khác biệt kiến trúc chính so với Node.js là engine. Node.js sử dụng V8 (engine của Chrome). Bun sử dụng JavaScriptCore (engine của Safari). Cả hai đều là engine trưởng thành, đạt chuẩn production, nhưng chúng đánh đổi khác nhau. JavaScriptCore có xu hướng khởi động nhanh hơn và tiêu thụ bộ nhớ ít hơn. V8 có xu hướng có throughput đỉnh tốt hơn cho các tính toán chạy lâu. Trong thực tế, những khác biệt này nhỏ hơn bạn nghĩ cho hầu hết các workload.
Điểm khác biệt lớn khác: Bun được viết bằng Zig, một ngôn ngữ lập trình hệ thống nằm ở cùng mức với C nhưng với các đảm bảo an toàn bộ nhớ tốt hơn. Đây là lý do Bun có thể mạnh mẽ về hiệu năng — Zig cho bạn kiểu kiểm soát cấp thấp mà C cung cấp mà không có mật độ "footgun" của C.
# Kiểm tra phiên bản Bun của bạn
bun --version
# Chạy file TypeScript trực tiếp — không cần tsconfig, không cần bước biên dịch
bun run server.ts
# Cài đặt gói
bun install
# Chạy test
bun test
# Bundle cho production
bun build ./src/index.ts --outdir ./distĐó là một file nhị phân làm công việc của node + npm + esbuild + vitest. Yêu hay ghét, đó là một sự giảm thiểu độ phức tạp đáng kể.
Các Tuyên Bố Về Tốc Độ — Benchmark Trung Thực#
Cho tôi nói thẳng về điều này: benchmark marketing của Bun là cherry-picked. Không gian lận — cherry-picked. Chúng cho thấy các kịch bản mà Bun hoạt động tốt nhất, đó chính xác là điều bạn mong đợi từ tài liệu marketing. Vấn đề là mọi người ngoại suy từ những benchmark đó để tuyên bố Bun "nhanh hơn 25 lần" ở mọi thứ, điều này hoàn toàn không đúng.
Đây là những nơi Bun thực sự, có ý nghĩa nhanh hơn:
Thời Gian Khởi Động#
Đây là lợi thế thực sự lớn nhất của Bun và nó không hề sát nút.
# Đo thời gian khởi động — chạy mỗi cái 100 lần
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
# Kết quả thông thường:
# node: ~40ms
# bun: ~6msĐó là chênh lệch khoảng 6-7 lần thời gian khởi động. Cho các script, công cụ CLI, và serverless function nơi cold start quan trọng, điều này có ý nghĩa. Cho một tiến trình server chạy lâu khởi động một lần và chạy hàng tuần, nó không quan trọng.
Cài Đặt Gói#
Đây là lĩnh vực khác mà Bun vượt trội hoàn toàn.
# Benchmark cài đặt sạch — xóa node_modules và lockfile trước
rm -rf node_modules bun.lockb package-lock.json
# Đo npm
time npm install
# Real: ~18.4s (dự án cỡ trung bình thông thường)
# Đo bun
time bun install
# Real: ~2.1sĐó là chênh lệch 8-9 lần, và nó nhất quán. Lý do chủ yếu là:
- Lockfile nhị phân —
bun.lockblà định dạng nhị phân, không phải JSON. Đọc và ghi nhanh hơn. - Cache toàn cục — Bun duy trì cache module toàn cục nên việc cài đặt lại qua các dự án chia sẻ các gói đã tải xuống.
- I/O của Zig — Bản thân trình quản lý gói được viết bằng Zig, không phải JavaScript. Các thao tác I/O file gần với phần cứng hơn.
- Chiến lược symlink — Bun sử dụng hardlink từ cache toàn cục thay vì sao chép file.
Throughput HTTP Server#
HTTP server tích hợp của Bun nhanh, nhưng các so sánh cần bối cảnh.
# Benchmark nhanh với bombardier
# Test phản hồi "Hello World" đơn giản
# Bun server
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sec: ~105,000
# Node.js (module http native)
bombardier -c 100 -d 10s http://localhost:3001
# Requests/sec: ~48,000
# Node.js (Express)
bombardier -c 100 -d 10s http://localhost:3002
# Requests/sec: ~15,000Bun so với Node.js thuần: khoảng 2 lần cho phản hồi trivial. Bun so với Express: khoảng 7 lần, nhưng không công bằng vì Express thêm overhead middleware. Khi bạn thêm logic thực tế — truy vấn database, xác thực, serialization JSON dữ liệu thực — khoảng cách thu hẹp đáng kể.
Nơi Sự Khác Biệt Không Đáng Kể#
Tính toán CPU-bound:
// fibonacci.ts — cái này phụ thuộc engine, không phụ thuộc runtime
function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
const start = performance.now();
console.log(fib(42));
console.log(`${(performance.now() - start).toFixed(0)}ms`);bun run fibonacci.ts # ~1650ms
node fibonacci.ts # ~1580msNode.js (V8) thực sự thắng nhẹ ở đây. Trình biên dịch JIT của V8 mạnh mẽ hơn ở các vòng lặp hot. Cho công việc CPU-bound, sự khác biệt engine là ngang ngửa — đôi khi V8 thắng, đôi khi JSC thắng, và sự khác biệt nằm trong phạm vi nhiễu.
Cách Chạy Benchmark Của Riêng Bạn#
Đừng tin benchmark của ai, bao gồm cả của tôi. Đây là cách đo những gì quan trọng cho workload cụ thể của bạn:
# Cài đặt hyperfine để benchmark đúng cách
brew install hyperfine # macOS
# hoặc: cargo install hyperfine
# Benchmark khởi động + thực thi ứng dụng thực của bạn
hyperfine --warmup 3 \
'node dist/server.js' \
'bun src/server.ts' \
--prepare 'sleep 0.1'
# Cho HTTP server, sử dụng bombardier hoặc wrk
# Quan trọng: test với payload thực tế, không phải "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
# So sánh bộ nhớ
/usr/bin/time -v node server.js # Linux
/usr/bin/time -l bun server.ts # macOSQuy tắc chung: nếu bottleneck của bạn là I/O (file system, mạng, database), lợi thế của Bun khiêm tốn. Nếu bottleneck của bạn là thời gian khởi động hoặc tốc độ toolchain, Bun thắng lớn. Nếu bottleneck của bạn là tính toán thuần, nó ngang ngửa.
Bun Với Vai Trò Trình Quản Lý Gói#
Đây là nơi tôi đã chuyển hoàn toàn. Ngay cả trên các dự án mà tôi chạy Node.js trong production, tôi sử dụng bun install cho phát triển local và CI. Nó nhanh hơn, và khả năng tương thích rất tốt.
Cơ Bản#
# Cài đặt tất cả dependencies từ package.json
bun install
# Thêm dependency
bun add express
# Thêm dev dependency
bun add -d vitest
# Xóa dependency
bun remove express
# Cập nhật dependency
bun update express
# Cài đặt phiên bản cụ thể
bun add express@4.18.2Nếu bạn đã dùng npm hoặc yarn, điều này hoàn toàn quen thuộc. Các flag hơi khác (-d thay vì --save-dev), nhưng mô hình tư duy giống hệt.
Tình Hình Lockfile#
Bun sử dụng bun.lockb, một lockfile nhị phân. Đây vừa là siêu năng lực vừa là điểm ma sát lớn nhất.
Điều tốt: Nó nhanh hơn đáng kể khi đọc và ghi. Định dạng nhị phân có nghĩa là Bun có thể parse lockfile trong vài microsecond, không phải hàng trăm millisecond mà npm tốn khi parse package-lock.json.
Điều xấu: Bạn không thể xem nó trong diff. Nếu bạn trong team và ai đó cập nhật dependency, bạn không thể nhìn diff lockfile trong PR và thấy gì đã thay đổi. Điều này quan trọng hơn những người ủng hộ tốc độ muốn thừa nhận.
# Bạn có thể xuất lockfile sang định dạng đọc được
bun bun.lockb > lockfile-dump.txt
# Hoặc sử dụng output dạng text tích hợp
bun install --yarn
# Điều này tạo yarn.lock bên cạnh bun.lockbCách tiếp cận của tôi: Tôi commit bun.lockb vào repo và cũng tạo yarn.lock hoặc package-lock.json làm bản dự phòng đọc được. Dây nịt và dây đai.
Hỗ Trợ Workspace#
Bun hỗ trợ workspace kiểu npm/yarn:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# Cài đặt dependencies cho tất cả workspace
bun install
# Chạy script trong workspace cụ thể
bun run --filter packages/shared build
# Thêm dependency vào workspace cụ thể
bun add react --filter apps/webHỗ trợ workspace vững chắc và đã cải thiện đáng kể. Khoảng trống chính so với pnpm là phân giải dependency workspace của Bun ít nghiêm ngặt hơn — sự nghiêm ngặt của pnpm là một tính năng cho monorepo vì nó bắt phantom dependency.
Tương Thích Với Dự Án Hiện Tại#
Bạn có thể đưa bun install vào gần như bất kỳ dự án Node.js hiện có nào. Nó đọc package.json, tuân thủ .npmrc cho cấu hình registry, và xử lý peerDependencies chính xác. Quá trình chuyển đổi thường là:
# Bước 1: Xóa lockfile và node_modules hiện có
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
# Bước 2: Cài đặt với Bun
bun install
# Bước 3: Xác nhận ứng dụng vẫn hoạt động
bun run dev
# hoặc: node dist/server.js (trình quản lý gói Bun, runtime Node)Tôi đã làm điều này trên hàng chục dự án và không gặp vấn đề gì với bản thân trình quản lý gói. Gotcha duy nhất là nếu pipeline CI của bạn tìm kiếm cụ thể package-lock.json — bạn sẽ cần cập nhật nó để xử lý bun.lockb.
Tương Thích Node.js#
Đây là phần tôi phải cẩn thận nhất, vì tình hình thay đổi mỗi tháng. Tính đến đầu năm 2026, đây là bức tranh trung thực.
Những Gì Hoạt Động#
Phần lớn các gói npm hoạt động mà không cần sửa đổi. Bun triển khai hầu hết các module tích hợp của Node.js:
// Tất cả những cái này đều hoạt động như mong đợi trong Bun
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { Readable, Writable } from "node:stream";
import http from "node:http";
import https from "node:https";
import { URL, URLSearchParams } from "node:url";
import os from "node:os";
import child_process from "node:child_process";Cả CommonJS và ESM đều hoạt động. require() và import có thể cùng tồn tại. TypeScript chạy mà không cần bước biên dịch — Bun loại bỏ type tại thời điểm parse.
Các framework hoạt động:
- Express — hoạt động, bao gồm hệ sinh thái middleware
- Fastify — hoạt động
- Hono — hoạt động (và rất tuyệt với Bun)
- Next.js — hoạt động với một số lưu ý (thêm chi tiết bên dưới)
- Prisma — hoạt động
- Drizzle ORM — hoạt động
- Socket.io — hoạt động
Những Gì Không Hoạt Động (hoặc Có Vấn Đề)#
Các khoảng trống có xu hướng rơi vào một vài danh mục:
Native addon (node-gyp): Nếu một gói sử dụng addon C++ được biên dịch với node-gyp, nó có thể không hoạt động với Bun. Bun có hệ thống FFI riêng và hỗ trợ nhiều module native, nhưng coverage chưa đạt 100%. Ví dụ, bcrypt (phiên bản native) đã có vấn đề — sử dụng bcryptjs thay thế.
# Kiểm tra xem gói có sử dụng native addon không
ls node_modules/your-package/binding.gyp # Nếu file này tồn tại, đó là nativeCác internal cụ thể của Node.js: Một số gói truy cập vào internal của Node.js như process.binding() hoặc sử dụng API riêng của V8. Những cái này sẽ không hoạt động trong Bun vì nó chạy trên JavaScriptCore.
// Cái này SẼ KHÔNG hoạt động trong Bun — riêng của V8
const v8 = require("v8");
v8.serialize({ data: "test" });
// Cái này SẼ hoạt động — sử dụng tương đương của Bun hoặc cách tiếp cận cross-runtime
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));Worker thread: Bun hỗ trợ Web Worker và node:worker_threads, nhưng có những trường hợp biên. Một số mẫu sử dụng nâng cao — đặc biệt xung quanh SharedArrayBuffer và Atomics — có thể hoạt động khác.
Module vm: node:vm có hỗ trợ một phần. Nếu code hoặc dependency của bạn sử dụng vm.createContext() rộng rãi (một số template engine làm vậy), hãy test kỹ lưỡng.
Trình Theo Dõi Tương Thích#
Bun duy trì trình theo dõi tương thích chính thức. Kiểm tra nó trước khi cam kết sử dụng Bun cho dự án:
# Chạy kiểm tra tương thích tích hợp của Bun trên dự án của bạn
bun --bun node_modules/.bin/your-tool
# Flag --bun buộc runtime của Bun ngay cả cho script node_modulesKhuyến nghị của tôi: đừng giả định tương thích. Chạy bộ test dưới Bun trước khi quyết định. Mất năm phút và tiết kiệm hàng giờ debug.
# Kiểm tra tương thích nhanh — chạy toàn bộ bộ test dưới Bun
bun test # Nếu bạn dùng bun test runner
# hoặc
bun run vitest # Nếu bạn dùng vitestAPI Tích Hợp Của Bun#
Đây là nơi Bun trở nên thú vị. Thay vì chỉ triển khai lại API của Node.js, Bun cung cấp API riêng được thiết kế để đơn giản hơn và nhanh hơn.
Bun.serve() — HTTP Server Tích Hợp#
Đây là API tôi sử dụng nhiều nhất. Nó sạch, nhanh, và hỗ trợ WebSocket được tích hợp ngay.
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello from Bun!", {
headers: { "Content-Type": "text/plain" },
});
}
if (url.pathname === "/api/users") {
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
return Response.json(users);
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);Một vài điều cần chú ý:
- Request/Response theo tiêu chuẩn Web — không có API độc quyền. Handler
fetchnhậnRequesttiêu chuẩn và trả vềResponsetiêu chuẩn. Nếu bạn đã viết Cloudflare Worker, cảm giác giống hệt. Response.json()— helper phản hồi JSON tích hợp.- Không cần import —
Bun.servelà global. Không cầnrequire("http").
Đây là ví dụ thực tế hơn với routing, phân tích JSON body, và xử lý lỗi:
import { Database } from "bun:sqlite";
const db = new Database("app.db");
db.run(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
const server = Bun.serve({
port: process.env.PORT || 3000,
async fetch(req) {
const url = new URL(req.url);
const method = req.method;
try {
// GET /api/todos
if (url.pathname === "/api/todos" && method === "GET") {
const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
return Response.json(todos);
}
// POST /api/todos
if (url.pathname === "/api/todos" && method === "POST") {
const body = await req.json();
if (!body.title || typeof body.title !== "string") {
return Response.json({ error: "Title is required" }, { status: 400 });
}
const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
const todo = stmt.get(body.title);
return Response.json(todo, { status: 201 });
}
// DELETE /api/todos/:id
const deleteMatch = url.pathname.match(/^\/api\/todos\/(\d+)$/);
if (deleteMatch && method === "DELETE") {
const id = parseInt(deleteMatch[1], 10);
db.run("DELETE FROM todos WHERE id = ?", [id]);
return new Response(null, { status: 204 });
}
return Response.json({ error: "Not found" }, { status: 404 });
} catch (error) {
console.error("Request error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
});
console.log(`Server running on port ${server.port}`);Đó là một API CRUD đầy đủ với SQLite trong khoảng 50 dòng. Không Express, không ORM, không chuỗi middleware. Cho các API nhỏ và công cụ nội bộ, đây là setup mặc định của tôi bây giờ.
Bun.file() và Bun.write() — I/O File#
API file của Bun đơn giản một cách sảng khoái so với fs.readFile():
// Đọc file
const file = Bun.file("./config.json");
const text = await file.text(); // Đọc dạng string
const json = await file.json(); // Parse JSON trực tiếp
const bytes = await file.arrayBuffer(); // Đọc dạng ArrayBuffer
const stream = file.stream(); // Đọc dạng ReadableStream
// Metadata file
console.log(file.size); // Kích thước tính bằng byte
console.log(file.type); // Loại MIME (vd: "application/json")
// Ghi file
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.json", JSON.stringify({ key: "value" }));
await Bun.write("./copy.png", Bun.file("./original.png"));
// Ghi body Response vào file
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);API Bun.file() là lazy — nó không đọc file cho đến khi bạn gọi .text(), .json(), v.v. Điều này có nghĩa bạn có thể truyền tham chiếu Bun.file() xung quanh mà không phát sinh chi phí I/O cho đến khi bạn thực sự cần dữ liệu.
Hỗ Trợ WebSocket Tích Hợp#
WebSocket là first-class trong Bun.serve():
const server = Bun.serve({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/ws") {
const upgraded = server.upgrade(req, {
data: {
userId: url.searchParams.get("userId"),
joinedAt: Date.now(),
},
});
if (!upgraded) {
return new Response("WebSocket upgrade failed", { status: 400 });
}
return undefined;
}
return new Response("Use /ws for WebSocket connections");
},
websocket: {
open(ws) {
console.log(`Client connected: ${ws.data.userId}`);
ws.subscribe("chat");
},
message(ws, message) {
// Broadcast tới tất cả subscriber
server.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws) {
console.log(`Client disconnected: ${ws.data.userId}`);
ws.unsubscribe("chat");
},
},
});Mẫu server.publish() và ws.subscribe() là pub/sub tích hợp. Không Redis, không thư viện WebSocket riêng. Cho các tính năng real-time đơn giản, điều này cực kỳ tiện lợi.
SQLite Tích Hợp với bun:sqlite#
Điều này khiến tôi bất ngờ nhất. Bun đi kèm SQLite ngay trong runtime:
import { Database } from "bun:sqlite";
// Mở hoặc tạo database
const db = new Database("myapp.db");
// Chế độ WAL cho hiệu năng đọc đồng thời tốt hơn
db.exec("PRAGMA journal_mode = WAL");
// Tạo bảng
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Prepared statement (có thể tái sử dụng, nhanh hơn cho truy vấn lặp lại)
const insertUser = db.prepare(
"INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
const findByEmail = db.prepare(
"SELECT * FROM users WHERE email = $email"
);
// Sử dụng
const user = insertUser.get({
$email: "alice@example.com",
$name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
// Transaction
const insertMany = db.transaction((users: { email: string; name: string }[]) => {
for (const user of users) {
insertUser.run({ $email: user.email, $name: user.name });
}
return users.length;
});
const count = insertMany([
{ email: "bob@example.com", name: "Bob" },
{ email: "carol@example.com", name: "Carol" },
]);
console.log(`Inserted ${count} users`);Đây là SQLite đồng bộ với hiệu năng của thư viện C (vì nó chính là — Bun nhúng libsqlite3 trực tiếp). Cho công cụ CLI, ứng dụng local-first, và các service nhỏ, SQLite tích hợp nghĩa là không có dependency bên ngoài cho lớp dữ liệu.
Bun Test Runner#
bun test là thay thế drop-in cho Jest trong hầu hết các trường hợp. Nó sử dụng cùng API describe/it/expect và hỗ trợ hầu hết các matcher của Jest.
Sử Dụng Cơ Bản#
// math.test.ts
import { describe, it, expect } from "bun:test";
describe("math utilities", () => {
it("adds numbers correctly", () => {
expect(1 + 2).toBe(3);
});
it("handles floating point", () => {
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
});# Chạy tất cả test
bun test
# Chạy file cụ thể
bun test math.test.ts
# Chạy test khớp mẫu
bun test --test-name-pattern "adds numbers"
# Chế độ watch
bun test --watch
# Coverage
bun test --coverageMocking#
Bun hỗ trợ mocking tương thích Jest:
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
// Mock một module
mock.module("./database", () => ({
query: mock(() => [{ id: 1, name: "Alice" }]),
}));
describe("fetchUsers", () => {
it("returns users from database", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Alice");
});
});
// Spy trên method của object
describe("console", () => {
it("tracks console.log calls", () => {
const logSpy = spyOn(console, "log");
console.log("test message");
expect(logSpy).toHaveBeenCalledWith("test message");
logSpy.mockRestore();
});
});Bun Test vs. Vitest — So Sánh Trung Thực#
Tôi sử dụng Vitest cho dự án này (và hầu hết các dự án). Đây là lý do tôi chưa chuyển hoàn toàn:
Nơi bun test thắng:
- Tốc độ khởi động.
bun testbắt đầu thực thi test nhanh hơn cả thời gian Vitest tải xong config. - Không cần config. Không cần
vitest.config.tscho setup cơ bản. - TypeScript tích hợp. Không cần bước chuyển đổi.
Nơi Vitest vẫn thắng:
- Hệ sinh thái: Vitest có nhiều plugin hơn, tích hợp IDE tốt hơn, và cộng đồng lớn hơn.
- Cấu hình: Hệ thống config của Vitest linh hoạt hơn. Reporter tùy chỉnh, file setup phức tạp, nhiều môi trường test.
- Chế độ browser: Vitest có thể chạy test trong browser thực. Bun không thể.
- Tương thích: Một số thư viện testing (Testing Library, MSW) đã được test kỹ lưỡng hơn với Vitest/Jest.
- Snapshot testing: Cả hai đều hỗ trợ, nhưng triển khai của Vitest hoàn thiện hơn với output diff tốt hơn.
Cho dự án mới với nhu cầu testing đơn giản, tôi sẽ dùng bun test. Cho dự án đã ổn định với Testing Library, MSW, và mocking phức tạp, tôi giữ Vitest.
Bun Bundler#
bun build là bundler JavaScript/TypeScript nhanh. Nó không phải thay thế webpack — nó thuộc hạng esbuild hơn: nhanh, có chính kiến, và tập trung vào các trường hợp phổ biến.
Bundling Cơ Bản#
# Bundle một entry point duy nhất
bun build ./src/index.ts --outdir ./dist
# Bundle cho các target khác nhau
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target bun
bun build ./src/index.ts --outdir ./dist --target node
# Minify
bun build ./src/index.ts --outdir ./dist --minify
# Tạo sourcemap
bun build ./src/index.ts --outdir ./dist --sourcemap externalAPI Lập Trình#
const result = await Bun.build({
entrypoints: ["./src/index.ts", "./src/worker.ts"],
outdir: "./dist",
target: "browser",
minify: {
whitespace: true,
identifiers: true,
syntax: true,
},
splitting: true, // Tách code
sourcemap: "external",
external: ["react", "react-dom"], // Không bundle những cái này
naming: "[dir]/[name]-[hash].[ext]",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});
if (!result.success) {
console.error("Build failed:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
for (const output of result.outputs) {
console.log(`${output.path} — ${output.size} bytes`);
}Tree-Shaking#
Bun hỗ trợ tree-shaking cho ESM:
// utils.ts
export function used() {
return "I'll be in the bundle";
}
export function unused() {
return "I'll be tree-shaken away";
}
// index.ts
import { used } from "./utils";
console.log(used());bun build ./src/index.ts --outdir ./dist --minify
# Hàm `unused` sẽ không xuất hiện trong outputNơi Bun Build Còn Thiếu#
- Không bundle CSS — Bạn cần công cụ riêng cho CSS (PostCSS, Lightning CSS, Tailwind CLI).
- Không tạo HTML — Nó bundle JavaScript/TypeScript, không phải ứng dụng web đầy đủ.
- Hệ sinh thái plugin — esbuild có hệ sinh thái plugin lớn hơn nhiều. API plugin của Bun tương thích nhưng cộng đồng nhỏ hơn.
- Tách code nâng cao — Webpack và Rollup vẫn cung cấp chiến lược chunk tinh vi hơn.
Để build thư viện hoặc bundle JS cho ứng dụng web đơn giản, bun build rất tuyệt. Cho build ứng dụng phức tạp với CSS module, tối ưu hình ảnh, và chiến lược chunk tùy chỉnh, bạn vẫn cần bundler đầy đủ.
Bun Macro#
Một tính năng thực sự độc đáo: thực thi code tại thời điểm biên dịch qua macro.
// build-info.ts — file này chạy tại THỜI ĐIỂM BUILD, không phải runtime
export function getBuildInfo() {
return {
builtAt: new Date().toISOString(),
gitSha: require("child_process")
.execSync("git rev-parse --short HEAD")
.toString()
.trim(),
nodeVersion: process.version,
};
}// app.ts
import { getBuildInfo } from "./build-info" with { type: "macro" };
// getBuildInfo() thực thi tại thời điểm bundle
// Kết quả được inline như giá trị tĩnh
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);Sau khi bundle, getBuildInfo() được thay thế bằng object literal — không có lời gọi hàm tại runtime, không import child_process. Code đã chạy trong quá trình build và kết quả được inline. Điều này mạnh mẽ cho việc nhúng metadata build, feature flag, hoặc cấu hình theo môi trường.
Sử Dụng Bun Với Next.js#
Đây là câu hỏi tôi được hỏi nhiều nhất, nên cho tôi nói rất cụ thể.
Những Gì Hoạt Động Hôm Nay#
Bun làm trình quản lý gói cho Next.js — hoạt động hoàn hảo:
# Sử dụng Bun để cài đặt dependency, sau đó dùng Node.js để chạy Next.js
bun install
bun run dev # Thực ra chạy script "dev" qua Node.js theo mặc định
bun run build
bun run startĐây là điều tôi làm cho mọi dự án Next.js. Lệnh bun run <script> đọc phần scripts trong package.json và thực thi nó. Mặc định, nó sử dụng Node.js của hệ thống cho việc thực thi thực tế. Bạn có được cài đặt gói nhanh của Bun mà không thay đổi runtime.
Runtime Bun cho phát triển Next.js:
# Buộc Next.js chạy dưới runtime của Bun
bun --bun run devĐiều này hoạt động cho phát triển trong hầu hết trường hợp. Flag --bun bảo Bun sử dụng runtime riêng thay vì ủy quyền cho Node.js. Hot module replacement hoạt động. API route hoạt động. Server component hoạt động.
Những Gì Vẫn Thử Nghiệm#
Runtime Bun cho build production Next.js:
# Build với runtime Bun
bun --bun run build
# Khởi động server production với runtime Bun
bun --bun run startĐiều này hoạt động cho nhiều dự án nhưng tôi đã gặp trường hợp biên:
- Một số hành vi middleware khác biệt — Nếu bạn sử dụng middleware Next.js phụ thuộc vào API riêng của Node.js, bạn có thể gặp vấn đề tương thích.
- Tối ưu hình ảnh — Pipeline tối ưu hình ảnh của Next.js sử dụng sharp, là native addon. Nó hoạt động với Bun, nhưng tôi đã thấy vấn đề thỉnh thoảng.
- ISR (Incremental Static Regeneration) — Hoạt động, nhưng tôi chưa stress-test nó dưới Bun trong production.
Khuyến Nghị Của Tôi Cho Next.js#
Sử dụng Bun làm trình quản lý gói. Sử dụng Node.js làm runtime. Điều này cho bạn lợi ích tốc độ của bun install mà không có rủi ro tương thích.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# Quy trình hàng ngày
bun install # Cài đặt gói nhanh
bun run dev # Chạy "next dev" qua Node.js
bun run build # Chạy "next build" qua Node.jsKhi khả năng tương thích Node.js của Bun đạt 100% cho cách sử dụng nội bộ của Next.js (nó gần rồi, nhưng chưa đạt), tôi sẽ chuyển. Cho đến lúc đó, bản thân trình quản lý gói đã tiết kiệm đủ thời gian để biện minh cho việc cài đặt.
Docker Với Bun#
Image Docker chính thức của Bun được bảo trì tốt và sẵn sàng cho production.
Dockerfile Cơ Bản#
FROM oven/bun:1 AS base
WORKDIR /app
# Cài đặt dependency
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Build (nếu cần)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Production
FROM base AS production
WORKDIR /app
# Không chạy với quyền root
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]Build Nhiều Giai Đoạn Cho Image Tối Thiểu#
# Giai đoạn build: image Bun đầy đủ với tất cả dependency
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build ./src/index.ts --target bun --outdir ./dist --minify
# Giai đoạn runtime: base image nhỏ hơn
FROM oven/bun:1-slim AS runtime
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]Biên Dịch Thành File Nhị Phân Duy Nhất#
Đây là một trong những tính năng killer của Bun cho deployment:
# Biên dịch ứng dụng thành file thực thi duy nhất
bun build --compile ./src/server.ts --outfile server
# Output là binary độc lập — không cần Bun hoặc Node.js để chạy
./server# Image Docker siêu tối thiểu sử dụng binary đã biên dịch
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --compile ./src/server.ts --outfile server
# Image cuối cùng — chỉ binary
FROM debian:bookworm-slim
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=builder /app/server ./server
EXPOSE 3000
CMD ["./server"]Binary đã biên dịch thường 50-90 MB (nó bundle runtime Bun). Lớn hơn binary Go nhưng nhỏ hơn nhiều so với cài đặt Node.js đầy đủ cộng node_modules. Cho deployment container hóa, tính chất tự chứa là đơn giản hóa đáng kể.
So Sánh Kích Thước#
# Image Node.js
docker images | grep node
# node:20-slim ~180MB
# Image Bun
docker images | grep bun
# oven/bun:1-slim ~130MB
# Binary đã biên dịch trên debian:bookworm-slim
# ~80MB base + ~70MB binary = ~150MB tổng
# vs. Alpine với Node.js
# node:20-alpine ~130MB + node_modulesCách tiếp cận binary loại bỏ hoàn toàn node_modules khỏi image cuối cùng. Không npm install trong production. Không bề mặt tấn công chuỗi cung ứng từ hàng trăm gói. Chỉ một file.
Mô Hình Migration#
Nếu bạn đang cân nhắc chuyển sang Bun, đây là con đường tăng dần mà tôi khuyến nghị:
Giai Đoạn 1: Chỉ Trình Quản Lý Gói (Không Rủi Ro)#
# Thay thế npm/yarn/pnpm bằng bun install
# Thay đổi pipeline CI của bạn:
# Trước:
npm ci
# Sau:
bun install --frozen-lockfileKhông thay đổi code. Không thay đổi runtime. Chỉ cài đặt nhanh hơn. Nếu bất cứ thứ gì hỏng (sẽ không), hoàn nguyên bằng cách xóa bun.lockb và chạy npm install.
Giai Đoạn 2: Script và Tooling#
# Sử dụng bun để chạy script phát triển
bun run dev
bun run lint
bun run format
# Sử dụng bun cho script chạy một lần
bun run scripts/seed-database.ts
bun run scripts/migrate.tsVẫn sử dụng Node.js làm runtime cho ứng dụng thực. Nhưng script được lợi từ khởi động nhanh hơn và hỗ trợ TypeScript native của Bun.
Giai Đoạn 3: Test Runner (Rủi Ro Trung Bình)#
# Thay thế vitest/jest bằng bun test cho bộ test đơn giản
bun test
# Giữ vitest cho setup test phức tạp
# (Testing Library, MSW, môi trường tùy chỉnh)Chạy toàn bộ bộ test dưới bun test. Nếu mọi thứ pass, bạn đã loại bỏ một devDependency. Nếu một số test fail do tương thích, giữ Vitest cho những cái đó và dùng bun test cho phần còn lại.
Giai Đoạn 4: Runtime Cho Service Mới (Rủi Ro Có Tính Toán)#
// Microservice hoặc API mới — bắt đầu với Bun ngay từ đầu
Bun.serve({
port: 3000,
fetch(req) {
// Service mới của bạn ở đây
},
});Không migrate service Node.js hiện có sang runtime Bun. Thay vào đó, viết service mới với Bun từ đầu. Điều này giới hạn phạm vi ảnh hưởng.
Giai Đoạn 5: Migration Runtime (Nâng Cao)#
# Chỉ sau khi test kỹ lưỡng:
# Thay thế node bằng bun cho service hiện có
# Trước:
node dist/server.js
# Sau:
bun dist/server.jsTôi chỉ khuyến nghị điều này cho service có test coverage tuyệt vời. Chạy load test dưới Bun trước khi chuyển production.
Biến Môi Trường và Cấu Hình#
Bun xử lý file .env tự động — không cần gói dotenv:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// Những cái này có sẵn mà không cần import
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Thay thế riêng của BunBun tải .env, .env.local, .env.production, v.v. tự động, theo cùng quy ước với Next.js. Một dependency ít hơn trong package.json.
Xử Lý Lỗi và Debug#
Output lỗi của Bun đã cải thiện đáng kể, nhưng vẫn chưa tinh tế bằng Node.js trong một số trường hợp:
# Debugger của Bun — hoạt động với VS Code
bun --inspect run server.ts
# Inspect-brk của Bun — tạm dừng ở dòng đầu tiên
bun --inspect-brk run server.tsCho VS Code, thêm cái này vào .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "launch",
"name": "Debug Bun",
"program": "${workspaceFolder}/src/server.ts",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
}
]
}Stack trace trong Bun nhìn chung chính xác và bao gồm source map cho TypeScript. Khoảng trống debug chính là một số công cụ debug riêng của Node.js (như ndb hoặc clinic.js) không hoạt động với Bun.
Cân Nhắc Bảo Mật#
Một vài điều cần suy nghĩ nếu bạn đang đánh giá Bun cho production:
Độ trưởng thành: Node.js đã chạy trong production hơn 15 năm. Mọi trường hợp biên trong phân tích HTTP, xử lý TLS, và xử lý stream đã được tìm thấy và sửa. Bun trẻ hơn. Nó được test tốt, nhưng bề mặt cho bug chưa được phát hiện lớn hơn.
Bản vá bảo mật: Team Bun phát hành cập nhật thường xuyên, nhưng team bảo mật Node.js có quy trình CVE chính thức, tiết lộ có phối hợp, và thành tích dài hơn. Cho ứng dụng quan trọng về bảo mật, điều này quan trọng.
Chuỗi cung ứng: Các tính năng tích hợp của Bun (SQLite, HTTP server, WebSocket) nghĩa là ít dependency npm hơn. Ít dependency nghĩa là bề mặt tấn công chuỗi cung ứng nhỏ hơn. Đây là lợi thế bảo mật thực sự.
# So sánh số lượng dependency
# Một dự án Express + SQLite + WebSocket điển hình:
npm ls --all | wc -l
# ~340 gói
# Cùng chức năng với tính năng tích hợp của Bun:
bun pm ls --all | wc -l
# ~12 gói (chỉ code ứng dụng của bạn)Đó là sự giảm có ý nghĩa trong số lượng gói mà bạn tin tưởng với workload production.
Tinh Chỉnh Hiệu Năng#
Một vài mẹo hiệu năng riêng cho Bun:
// Sử dụng tùy chọn Bun.serve() cho tinh chỉnh production
Bun.serve({
port: 3000,
// Tăng kích thước body request tối đa (mặc định 128MB)
maxRequestBodySize: 1024 * 1024 * 50, // 50MB
// Bật chế độ development cho trang lỗi tốt hơn
development: process.env.NODE_ENV !== "production",
// Tái sử dụng port (hữu ích cho restart không downtime)
reusePort: true,
fetch(req) {
return new Response("OK");
},
});// Sử dụng Bun.Transpiler cho chuyển đổi code runtime
const transpiler = new Bun.Transpiler({
loader: "tsx",
target: "browser",
});
const code = transpiler.transformSync(`
const App: React.FC = () => <div>Hello</div>;
export default App;
`);# Flag sử dụng bộ nhớ của Bun
bun --smol run server.ts # Giảm memory footprint (hơi chậm hơn)
# Đặt kích thước heap tối đa
BUN_JSC_forceRAMSize=512000000 bun run server.ts # ~512MB limitCác Gotcha Phổ Biến#
Sau một năm sử dụng Bun, đây là những thứ đã gây khó khăn cho tôi:
1. Hành Vi Global Fetch Khác Biệt#
// fetch Node.js 18+ và fetch của Bun hơi khác
// trong cách chúng xử lý một số header và redirect
// Bun theo redirect mặc định (giống browser)
// fetch Node.js cũng theo redirect, nhưng hành vi
// với một số status code (303, 307, 308) có thể khác
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // Rõ ràng về xử lý redirect
});2. Hành Vi Thoát Process#
// Bun thoát khi event loop trống
// Node.js đôi khi tiếp tục chạy do handle còn sót
// Nếu script Bun thoát bất ngờ, có thứ gì đó không
// giữ event loop hoạt động
// Cái này sẽ thoát ngay trong Bun:
setTimeout(() => {}, 0);
// Cái này sẽ tiếp tục chạy:
setTimeout(() => {}, 1000);
// (Bun thoát sau khi timeout kích hoạt)3. Cấu Hình TypeScript#
// Bun có mặc định tsconfig riêng
// Nếu bạn chia sẻ dự án giữa Bun và Node.js,
// hãy rõ ràng trong tsconfig.json:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // Thêm định nghĩa type cho Bun
}
}# Cài đặt Bun types
bun add -d @types/bun4. Hot Reload Trong Development#
# Bun có chế độ watch tích hợp
bun --watch run server.ts
# Cái này restart process khi file thay đổi
# Không phải HMR (Hot Module Replacement) — là restart đầy đủ
# Nhưng vì Bun khởi động rất nhanh, cảm giác tức thì5. File Cấu Hình bunfig.toml#
# bunfig.toml — file config của Bun (tùy chọn)
[install]
# Sử dụng registry riêng
registry = "https://npm.mycompany.com"
# Registry theo scope
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
[test]
# Cấu hình test
coverage = true
coverageReporter = ["text", "lcov"]
[run]
# Shell dùng cho bun run
shell = "bash"Kết Luận Của Tôi#
Sau một năm sử dụng production, đây là nơi tôi đã ổn định:
Nơi Tôi Sử Dụng Bun Hôm Nay#
Trình quản lý gói cho tất cả dự án — bao gồm blog Next.js này. bun install nhanh hơn, và tương thích gần như hoàn hảo. Tôi không thấy lý do để dùng npm hay yarn nữa. pnpm là thay thế duy nhất tôi cân nhắc (cho phân giải dependency nghiêm ngặt trong monorepo).
Runtime cho script và công cụ CLI — Bất kỳ file TypeScript nào tôi cần chạy một lần, tôi chạy với bun. Không cần bước biên dịch. Khởi động nhanh. Tải .env tích hợp. Nó đã thay thế hoàn toàn ts-node và tsx trong quy trình làm việc.
Runtime cho API nhỏ và công cụ nội bộ — Bun.serve() + bun:sqlite là stack cực kỳ hiệu quả cho công cụ nội bộ, webhook handler, và service nhỏ. Mô hình deployment "một binary, không dependency" rất hấp dẫn.
Test runner cho dự án đơn giản — Cho dự án có nhu cầu test đơn giản, bun test nhanh và không cần cấu hình.
Nơi Tôi Giữ Node.js#
Production Next.js — Không phải vì Bun không hoạt động, mà vì tỷ lệ rủi ro-lợi ích chưa biện minh được. Next.js là framework phức tạp với nhiều điểm tích hợp. Tôi muốn runtime đã được thử nghiệm thực chiến nhất bên dưới nó.
Service production quan trọng — Các server API chính của tôi chạy Node.js phía sau PM2. Hệ sinh thái monitoring, công cụ debug, kiến thức vận hành — tất cả đều Node.js. Bun sẽ đến đó, nhưng chưa đến lúc.
Bất cứ thứ gì có native addon — Nếu chuỗi dependency bao gồm addon native C++, tôi thậm chí không thử Bun. Không đáng để debug các vấn đề tương thích.
Team chưa quen với Bun — Giới thiệu Bun làm runtime cho team chưa bao giờ dùng nó thêm overhead nhận thức. Làm trình quản lý gói, OK. Làm runtime, hãy đợi cho đến khi team sẵn sàng.
Những Gì Tôi Đang Theo Dõi#
Trình theo dõi tương thích của Bun — Khi nó đạt 100% cho API Node.js mà tôi quan tâm, tôi sẽ đánh giá lại.
Hỗ trợ framework — Next.js, Remix, và SvelteKit đều có mức hỗ trợ Bun khác nhau. Khi một trong số chúng chính thức hỗ trợ Bun làm runtime production, đó là tín hiệu.
Áp dụng doanh nghiệp — Khi các công ty có SLA thực chạy Bun trong production và viết về nó, câu hỏi về độ trưởng thành được trả lời.
Dòng phát hành 1.2+ — Bun đang di chuyển nhanh. Tính năng ra mắt mỗi tuần. Bun mà tôi dùng hôm nay tốt hơn có ý nghĩa so với Bun tôi thử một năm trước.
Tổng Kết#
Bun không phải viên đạn bạc. Nó sẽ không làm ứng dụng chậm thành nhanh và sẽ không làm API thiết kế kém thành thiết kế tốt. Nhưng nó là cải thiện thực sự trong trải nghiệm phát triển cho hệ sinh thái JavaScript.
Điều tôi đánh giá cao nhất về Bun không phải bất kỳ tính năng đơn lẻ nào. Mà là sự giảm thiểu độ phức tạp toolchain. Một binary cài đặt gói, chạy TypeScript, bundle code, và chạy test. Không cần tsconfig.json cho script. Không Babel. Không config test runner riêng. Chỉ bun run your-file.ts và nó hoạt động.
Lời khuyên thực tế: bắt đầu với bun install. Không rủi ro, lợi ích tức thì. Sau đó thử bun run cho script. Sau đó đánh giá phần còn lại dựa trên nhu cầu cụ thể. Bạn không phải đi tất cả. Bun hoạt động hoàn hảo như thay thế một phần, và đó có lẽ là cách hầu hết mọi người nên sử dụng nó hôm nay.
Cảnh quan runtime JavaScript tốt hơn với Bun trong đó. Cạnh tranh cũng đang làm Node.js tốt hơn — Node.js 22+ đã nhanh hơn đáng kể, một phần nhờ áp lực từ Bun. Mọi người đều thắng.