Bun 快速的原生捆綁器現已推出 Beta 版。它可透過 bun build
CLI 命令或新的 Bun.build()
JavaScript API 使用。

使用捆綁器透過內建的 Bun.build()
函數或 bun build
CLI 命令來建置前端應用程式。
Bun.build({
entrypoints: ['./src/index.tsx'],
outdir: './build',
minify: true,
// additional config
});
bun build ./src/index.tsx --outdir ./build --minify
降低 JavaScript 的複雜性
JavaScript 最初是表單欄位的自動填寫功能,而如今它驅動著將火箭發射到太空的儀器。
毫不意外地,JavaScript 生態系統的複雜性已爆炸性增長。你該如何執行 TypeScript 檔案?你該如何建置/捆綁你的程式碼以用於生產環境?該套件是否適用於 ESM?你該如何載入僅限本機的設定?我是否需要安裝同層級依賴項 (peer dependencies)?我該如何讓原始碼對應 (sourcemaps) 正常運作?
複雜性會耗費時間,通常花在將工具黏合在一起或等待事物完成。安裝 npm 套件耗時太久。執行測試應該只需幾秒鐘(或更短)。在 2003 年上傳檔案到 FTP 伺服器只需毫秒,為何在 2023 年部署軟體卻需要幾分鐘?
多年來,我一直對 JavaScript 周遭的一切事物如此緩慢感到沮喪。當從儲存檔案到測試變更的迭代週期時間長到足以讓人本能地查看 Hacker News 時,就表示有些地方出錯了。
複雜性有其充分的理由。捆綁器和最小化器使網站載入速度更快。TypeScript 的編輯器內互動式文件讓開發人員更有效率。型別安全有助於在錯誤發佈給使用者之前就將其捕獲。版本化的套件形式的依賴項通常比複製檔案更容易維護。
當「一件事」被拆分到如此多隔離的工具之間時,「把一件事做好」的 Unix 哲學就會瓦解。
這就是我們正在建置 Bun 的原因,也是為何今天我們很高興推出 Bun 捆綁器的原因。
是的,一個新的捆綁器
有了新的捆綁器,捆綁現在成為 Bun 生態系統的一流元素,完整配備 bun build
CLI 命令、新的頂層 Bun.build
函數以及穩定的外掛系統。
我們決定 Bun 需要自己的捆綁器,原因有幾個。
凝聚力
捆綁器是協調和啟用所有其他工具的中繼工具,例如 JSX、TypeScript、CSS 模組和伺服器組件——所有這些都需要捆綁器整合才能運作。
如今,捆綁器已成為 JavaScript 生態系統中複雜性的來源。透過將捆綁引入 JavaScript 運行時環境,我們認為我們可以讓發布前端和全端程式碼更簡單、更快速。
- 快速外掛。 外掛在輕量級 Bun 程序中執行,啟動速度很快。
- 沒有多餘的轉譯。使用
target: "bun"
,捆綁器會產生針對 Bun 運行時環境最佳化的預先轉譯檔案,從而提高執行效能並避免不必要的重新轉譯。 - 統一的外掛 API。Bun 提供統一的外掛 API,可與捆綁器和運行時環境搭配使用。任何擴展 Bun 捆綁功能的外掛也可以用於擴展 Bun 的運行時環境功能。
- 運行時環境整合。建置會傳回
BuildArtifact
物件陣列,這些物件實作Blob
介面,可以直接傳遞到 HTTP API 中,例如new Response()
。運行時環境為BuildArtifact
實作了特殊的漂亮列印。 - 獨立可執行檔。捆綁器可以透過
--compile
旗標從 TypeScript 和 JavaScript 腳本產生獨立可執行檔。這些可執行檔完全是獨立的,並包含 Bun 運行時環境的副本。
很快,捆綁器將與 Bun 的 HTTP 伺服器 API (Bun.serve
) 整合,從而可以使用簡單的宣告式 API 來表示目前複雜的建置管線。稍後會有更多相關資訊。
效能
這點不會讓任何人感到驚訝。作為運行時環境,Bun 的程式碼庫已經包含快速解析和轉換原始碼的基礎(以 Zig 實作)。雖然有可能,但要與現有的原生捆綁器整合會很困難,而且跨程序通訊所涉及的額外負荷會損害效能。
最終結果證明了一切。在我們的基準測試(源自 esbuild 的 three.js 基準測試)中,Bun 比 esbuild 快 1.75 倍,比 Parcel 2 快 150 倍,比 Rollup + Terser 快 180 倍,比 Webpack 快 220 倍。
開發人員體驗
查看現有捆綁器的 API,我們看到了很大的改進空間。沒有人喜歡與捆綁器設定搏鬥。Bun 的捆綁器 API 旨在明確且不令人意外。說到這...
API
API 目前的設計盡可能簡潔。我們在這個初始版本中的目標是實作一組最小的功能集,這些功能集快速、穩定,並且在不犧牲效能的情況下,能夠滿足大多數現代使用案例。
以下是 API 目前的形式
interface Bun {
build(options: BuildOptions): Promise<BuildOutput>;
}
interface BuildOptions {
entrypoints: string[]; // required
outdir?: string; // default: no write (in-memory only)
target?: "browser" | "bun" | "node"; // "browser"
format?: "esm"; // later: "cjs" | "iife"
splitting?: boolean; // default false
plugins?: BunPlugin[]; // [] // see https://bun.dev.org.tw/docs/bundler/plugins
loader?: { [k in string]: string }; // see https://bun.dev.org.tw/docs/bundler/loaders
external?: string[]; // default []
sourcemap?: "none" | "inline" | "external"; // default "none"
root?: string; // default: computed from entrypoints
publicPath?: string; // e.g. http://mydomain.com/
naming?:
| string // equivalent to naming.entry
| { entry?: string; chunk?: string; asset?: string };
minify?:
| boolean // default false
| { identifiers?: boolean; whitespace?: boolean; syntax?: boolean };
}
其他捆綁器在追求功能完整性的過程中做出了糟糕的架構決策,最終削弱了效能;這是我們正在努力避免的錯誤。
模組系統
目前僅支援 format: "esm"
。我們計畫新增對其他模組系統和目標(例如 iife
)的支援。如果夠多人要求,我們也會新增 cjs
輸出支援(支援 CommonJS 輸入,但不支援輸出)。
目標
支援三個「目標」:"browser"
(預設值)、"bun"
和 "node"
。
browser
- TypeScript 和 JSX 會自動轉譯為原生 JavaScript。
- 模組在可用時會使用
"browser"
package.json"exports"
條件來解析 - 當在瀏覽器中匯入某些 Node.js API(例如
node:crypto
)時,Bun 會自動進行 polyfill,類似於 Webpack 4 的行為。目前禁止匯入 Bun 自己的 API,但我們可能會在未來重新檢視這一點。
bun
- 支援 Bun 和 Node.js API,並保持原樣。
- 模組使用 Bun 運行時環境使用的預設解析演算法來解析。
- 產生的捆綁包會標記特殊的
// @bun
pragma 註解,以指示它們是由 Bun 產生的。這向 Bun 運行時環境表明,該檔案在執行前不需要重新轉譯。協同作用!
node
目前,這與 target: "bun"
相同。未來,我們計畫自動 polyfill Bun API,例如 Bun
全域變數和 bun:*
內建模組。
檔案類型
捆綁器支援以下檔案類型
.js
.jsx
.ts
.tsx
- JavaScript 和 TypeScript 檔案。廢話。.txt
— 純文字檔案。這些檔案會以字串形式內嵌。.json
.toml
— 這些檔案會在編譯時解析,並以 JSON 形式內嵌。
其他所有內容都會被視為資產。資產會按原樣複製到 outdir
中,並且匯入會替換為檔案的相對路徑或 URL,例如 /images/logo.png
。
import logo from "./images/logo.png";
console.log(logo);
var logo = "./images/logo.png";
console.log(logo);
外掛
與運行時環境本身一樣,捆綁器的設計也可以透過外掛進行擴展。事實上,運行時環境外掛和捆綁器外掛之間完全沒有區別。
import YamlPlugin from "bun-plugin-yaml";
const plugin = YamlPlugin();
// register a runtime plugin
Bun.plugin(plugin);
// register a bundler plugin
Bun.build({
entrypoints: ["./src/index.ts"],
plugins: [plugin],
});
建置輸出
Bun.build
函數會傳回 Promise<BuildOutput>
,定義如下
interface BuildOutput {
outputs: BuildArtifact[];
success: boolean;
logs: Array<object>; // see docs for details
}
interface BuildArtifact extends Blob {
kind: "entry-point" | "chunk" | "asset" | "sourcemap";
path: string;
loader: Loader;
hash: string | null;
sourcemap: BuildArtifact | null;
}
outputs
陣列包含建置產生的所有檔案。每個成品都實作了 Blob
介面。
const build = await Bun.build({
/* */
});
for (const output of build.outputs) {
output.size; // file size in bytes
output.type; // MIME type of file
await output.arrayBuffer(); // => ArrayBuffer
await output.text(); // string
}
成品也包含以下額外屬性
kind | 此檔案的建置輸出類型。建置會產生捆綁的進入點、程式碼分割「區塊」、原始碼對應 (sourcemaps) 和複製的資產(例如圖片)。 |
path | 磁碟上檔案的絕對路徑,或檔案未寫入磁碟時的輸出路徑。 |
loader | 用於解譯檔案的載入器。請參閱捆綁器 > 載入器,以了解 Bun 如何將檔案副檔名對應到適當的內建載入器。 |
hash | 檔案內容的雜湊值。始終為資產定義。 |
sourcemap | 與此檔案對應的原始碼對應 (sourcemap) 的另一個 BuildArtifact ,如果已產生。僅為進入點和區塊定義。 |
與 BunFile
類似,BuildArtifact
物件可以直接傳遞到 new Response()
中。
const build = Bun.build({
/* */
});
const artifact = build.outputs[0];
// Content-Type is set automatically
return new Response(artifact);
當記錄 BuildArtifact
物件以方便除錯時,Bun 運行時環境會實作特殊的漂亮列印。
// build.ts
const build = Bun.build({/* */});
const artifact = build.outputs[0];
console.log(artifact);
bun run build.ts
BuildArtifact (entry-point) {
path: "./index.js",
loader: "tsx",
kind: "entry-point",
hash: "824a039620219640",
Blob (114 bytes) {
type: "text/javascript;charset=utf-8"
},
sourcemap: null
}
伺服器組件
Bun 的捆綁器透過 --server-components
旗標實驗性地支援 React 伺服器組件。我們將在本週稍後發布額外的文件和範例專案。
Tree shaking
Bun 的捆綁器支援未使用程式碼的 tree-shaking。捆綁時始終啟用此功能。
package.json "sideEffects"
欄位
Bun 支援 package.json
中的 "sideEffects": false
。這是一個提示捆綁器,表示該套件沒有副作用,並且可以更積極地消除無效程式碼。
__PURE__
註解
Bun 支援 __PURE__
註釋
function foo() {
return 123;
}
/** #__PURE__ */ foo();
由於 foo
沒有副作用,因此會產生一個空檔案
在 Webpack 的文件中了解更多資訊。
process.env.NODE_ENV
和 --define
Bun 支援 NODE_ENV
環境變數和 --define
CLI 旗標。這些通常用於在生產環境建置中條件式地包含程式碼。
如果 process.env.NODE_ENV
設定為 "production"
,Bun 將自動移除包裝在 if (process.env.NODE_ENV !== "production") { ... }
中的程式碼。
if (process.env.NODE_ENV !== "production") {
module.exports = require("./cjs/react.development.js");
} else {
module.exports = require("./cjs/react.production.min.js");
}
ES Module tree-shaking
ESM tree-shaking 同時支援 ESM 和 CommonJS 輸入檔案。當可以安全地執行時,Bun 的捆綁器將自動從 ESM 檔案中移除未使用的匯出。
import { foo } from "./foo.js";
console.log(foo);
export const bar = 123;
export const foo = 456;
未使用的 bar
匯出被消除,產生以下結果
// foo.js
var $foo = 456;
console.log($foo);
CommonJS tree-shaking
在有限的情況下,Bun 的捆綁器會自動將 CommonJS 轉換為 ESM,而不會產生運行時環境額外負荷。考慮這個簡單的範例
import { foo } from "./foo.js";
console.log(foo);
// foo.js
exports.foo = 123;
exports.bar = "this will be treeshaken";
Bun 將自動將 foo.js
轉換為 ESM,並對未使用的 exports
物件執行 tree-shaking。
// foo.js
var $foo = 123;
// entry.js
console.log($foo);
請注意,在許多情況下,CommonJS 的動態特性使這變得非常困難。例如,考慮以下三個檔案
// entry.js
export default require("./foo");
// foo.js
exports.foo = 123;
Object.assign(module.exports, require("./bar"));
// bar.js
exports.foobar = 123;
Bun 無法在不執行 foo.js
的情況下靜態判斷其匯出。(此外,Object.assign
可以被覆寫,使得在一般情況下無法進行靜態分析。)在這種情況下,Bun 不會對 exports
物件執行 tree-shaking;相反,它會注入一些 CommonJS 運行時環境程式碼,使其按預期運作。
CommonJS 包裝器
原始碼對應 (Source maps)
捆綁器同時支援內嵌和外部原始碼對應 (source maps)。
const build = await Bun.build({
entrypoints: ["./src/index.ts"],
// generates a *.js.map file alongside each output
sourcemap: "external",
// adds a base64-encoded `sourceMappingURL` to the end of each output file
sourcemap: "inline",
});
console.log(await build.outputs[0].sourcemap.json()); // => { version: 3, ... }
最小化器 (Minifier)
沒有最小化器 (minifier),JavaScript 捆綁器就不完整。此版本也推出了 Bun 內建的全新 JavaScript 最小化器 (minifier)。使用 minify: true
啟用最小化,或使用以下選項更精細地設定最小化行為
{
minify?: boolean | {
identifiers?: boolean; // default: false
whitespace?: boolean; // default: false
syntax?: boolean; // default: false
}
}
最小化器 (minifier) 能夠移除無效程式碼、重新命名識別符號、移除空白,以及智慧地濃縮和內嵌常數值。
// This comment will be removed!
console.log("this" + " " + "text" + " will" + " be " + "merged");
console.log("this text will be merged");
搶先看:Bun.App
捆綁器只是為更雄心勃勃的努力奠定基礎。在接下來的幾個月裡,我們將宣布 Bun.App
:「超級 API」,它將 Bun 的原生速度捆綁器、HTTP 伺服器和檔案系統路由器縫合在一起,形成一個有凝聚力的整體。
目標是讓使用者能夠輕鬆地用 Bun 和幾行程式碼來表達任何類型的應用程式
new Bun.App({
bundlers: [
{
name: "static-server",
outdir: "./out",
},
],
routers: [
{
mode: "static",
dir: "./public",
build: "static-server",
},
],
});
app.serve();
app.build();
const app = new Bun.App({
configs: [
{
name: "simple-http",
target: "bun",
outdir: "./.build/server",
// bundler config...
},
],
routers: [
{
mode: "handler",
handler: "./handler.tsx", // automatically included as entrypoint
prefix: "/api",
build: "simple-http",
},
],
});
app.serve();
app.build();
const projectRoot = process.cwd();
const app = new Bun.App({
configs: [
{
name: "react-ssr",
target: "bun",
outdir: "./.build/server",
// bundler config
},
{
name: "react-client",
target: "browser",
outdir: "./.build/client",
transform: {
exports: {
pick: ["default"],
},
},
},
],
routers: [
{
mode: "handler",
handler: "./handler.tsx",
build: "react-ssr",
style: "nextjs",
dir: projectRoot + "/pages",
},
{
mode: "build",
build: "react-client",
dir: "./pages",
// style: "build",
// dir: projectRoot + "/pages",
prefix: "_pages",
},
],
});
app.serve();
app.build();
此 API 仍在 積極討論中,並且可能會變更。
鳴謝
- bun 捆綁器和最小化器 (minifier) 的架構基於 esbuild 的設計,因此感謝 Evan Wallace (evanw)。
- 感謝 @paperclover 將 esbuild 的測試套件移植到 Bun。
- 感謝 @dylan-conway 實作原始碼對應 (source maps) 支援並修復了許多錯誤。