這篇整理自 2026 年 3 月在前端讀書會的分享,投影片是公開的。

為了這次分享,我照著 WebContainers 官方教學做了一個極簡的瀏覽器內 IDE:左邊編輯 Express 的程式碼,右邊即時預覽,下面是終端機。可以直接打開 demo 試試看,原始碼在 GitHub。

什麼是 WebContainers

WebContainers 是 StackBlitz 開發的瀏覽器執行環境,可以直接在瀏覽器分頁裡執行 Node.js、npm 和 shell 指令,不需要後端伺服器。StackBlitz.com 就是用它做的。

它能做到的事情包括:

  • 執行 Node.js,支援 npm install、ESM 和 CommonJS
  • 虛擬檔案系統,可以掛載、讀寫和監聽檔案
  • 類 POSIX 的 shell(jsh),輸入輸出都是串流
  • 在容器裡啟動的伺服器,會透過 Service Worker 拿到一個代理網址,外層頁面可以直接存取

在 demo 的終端機裡,可以像在本機一樣操作:

$ node --version
$ npm install express
$ node index.js

傳統做法的問題

要讓使用者在網頁上執行程式碼,傳統做法是在雲端開一台 VM 當沙盒。這可行,但有幾個代價:

  • 延遲:每個操作都要經過網路往返。
  • 成本:伺服器要隨使用者數量擴充,很貴。
  • 安全:使用者的程式碼跑在共用的伺服器上。
  • 啟動慢:開一台 VM 要好幾秒。

WebContainers 把這些事情都搬到使用者的瀏覽器裡:運算在本機完成,沒有網路延遲;不需要伺服器,靜態檔案透過 CDN 發送就好;執行環境關在瀏覽器的沙盒裡;啟動只要幾毫秒,不必等 VM。

這套做法已經被大規模使用,除了 StackBlitz 本身,Svelte、Angular、Nuxt 的互動式官方教學也都是用 WebContainers 做的。

底層架構

WebContainers 建立在四項瀏覽器技術之上。

WebContainers 的架構:主執行緒為每個 process 開一個 Web Worker,Worker 之間共用 SharedArrayBuffer,底層是 WebAssembly 每個 process 各有一個 Web Worker,共用同一塊記憶體;網路請求由 Service Worker 處理,見後文

WebAssembly

Node.js 的核心是用 C/C++ 寫的,瀏覽器只能執行 JavaScript 和 WebAssembly,沒辦法直接跑。WebContainers 的做法是把需要的部分編譯成 WebAssembly,Node.js 內建的 JavaScript 模組則直接交給瀏覽器本身的 V8 執行,不用在虛擬機裡再跑一個虛擬機。

open、fork 這類作業系統層級的呼叫,會被攔截下來,改用瀏覽器的 API 重新實作。

SharedArrayBuffer 與 Atomics

SharedArrayBuffer 是一塊可以在多個 Worker 之間共用的記憶體,每個 Worker 都能直接讀寫,不必透過 postMessage 傳訊息。

Atomics 在這之上提供同步機制:Atomics.wait() 讓 Worker 停下來等,Atomics.notify() 再把它叫醒。

Node.js 有很多同步 API,例如 fs.readFileSync,呼叫後要等結果回來才往下執行。瀏覽器裡的跨 Worker 溝通原本都是非同步的,靠這兩者搭配,WebContainers 才能模擬出同步、會阻塞的呼叫。

Service Worker

容器裡的 HTTP 伺服器並沒有真正的 TCP 連接埠,它跑在 Worker 裡。

當伺服器開始監聽某個 port,WebContainers 會替它產生一個專屬的 HTTPS 代理網址,例如 *.webcontainer-api.io。Service Worker 會攔截送往這個網址的請求,轉給容器裡的伺服器,再把回應傳回去。對 <iframe> 來說,就像在跟一台真的伺服器溝通。

Web Workers

瀏覽器的 JavaScript 預設只有一條主執行緒,任何阻塞的操作都會讓畫面卡住。

WebContainers 替每個 process 配一個獨立的 Web Worker,所以可以真的平行執行,例如一邊跑 dev server、一邊 npm install。另外,Atomics.wait() 不能在主執行緒呼叫,所以執行環境本身也一定要跑在 Worker 裡。

從啟動到預覽

用 @webcontainer/api 在瀏覽器裡跑起一台伺服器,有四個步驟:

  1. Boot:WebContainer.boot() 初始化執行環境,每次載入頁面只呼叫一次。
  2. Mount:container.mount(tree) 把專案的檔案寫進虛擬檔案系統。
  3. Spawn:container.spawn('jsh') 啟動 shell 或 Node.js process,輸入輸出以串流傳遞。
  4. Preview:伺服器開始監聽後,會觸發 server-ready 事件,拿到可以放進 <iframe> 的網址。

虛擬檔案系統

檔案系統存在執行環境的記憶體裡,外層頁面的 JavaScript 和容器裡的 Node.js process 共用同一份,完全不會碰到使用者的硬碟。

  • 任何一邊寫入,另一邊馬上看得到
  • 任何一邊改動,都會觸發 file watcher
  • 只存在記憶體裡,關掉分頁就消失

外層頁面和 Node.js process 透過同一份虛擬檔案系統讀寫 外層頁面和容器裡的 process 讀寫的是同一份檔案

外層頁面透過 container.fs 操作檔案:

API 用途
container.mount(tree) 用 FileSystemTree 物件初始化整個檔案系統
container.fs.readFile(path) 讀檔,回傳 Uint8Array 或字串
container.fs.writeFile(path, data) 寫入或覆寫檔案
container.fs.mkdir(path) 建立目錄
container.fs.rm(path) 刪除檔案或目錄
container.fs.watch(path, cb) 監聽變更,用法和 Node.js 的 fs.watch 一樣

要掛載的檔案有三種來源:

// 1. 直接寫在程式裡
await container.mount({ 'index.js': { file: { contents: src } } });

// 2. 從 API 取得,伺服器回傳 FileSystemTree 格式的 JSON
const tree = await fetch('/api/project').then((r) => r.json());
await container.mount(tree);

// 3. 二進位快照(官方推薦)
// 伺服器端:res.send(await snapshot('./my-project')),snapshot 來自 @webcontainer/snapshot
const buf = await fetch('/snapshot').then((r) => r.arrayBuffer());
await container.mount(buf);

Process 與終端機

spawn() 會在容器裡啟動一個 process,它的輸入輸出以 Web Streams 的形式傳回頁面。spawn() 回傳的物件有這些屬性和方法:

API 用途
output stdout 和 stderr 的 ReadableStream
input.getWriter() 寫入 stdin 的 WritableStream writer
exit process 結束時 resolve,值是結束代碼
kill() 終止 process
resize({ cols, rows }) 更新終端機的尺寸

demo 用 xterm.js 當終端機介面,把 shell 的輸出接到畫面,再把鍵盤輸入接回 stdin:

const shell = await container.spawn('jsh', {
	terminal: { cols, rows },
});

// shell 的輸出 → xterm.js
shell.output.pipeTo(
	new WritableStream({
		write(data) {
			terminal.write(data);
		},
	}),
);

// xterm.js 的鍵盤輸入 → shell 的 stdin
const input = shell.input.getWriter();
terminal.onData((data) => {
	input.write(data);
});

預覽伺服器

容器裡的 process 開始監聽某個 port 時,會觸發 server-ready 事件,並帶上代理網址。把網址放進 <iframe>,就能在同一頁即時預覽:

container.on('server-ready', (port, url) => {
	iframe.src = url;
});

iframe 的請求經過 Service Worker 轉發給容器裡的 Express 伺服器 server-ready 帶回代理網址,iframe 對它發出的請求由 Service Worker 攔截、轉給容器裡的伺服器

可以同時監聽多個 port,每個 port 都有自己的代理網址。

跨來源隔離:一定要設定的 header

WebContainers 依賴 SharedArrayBuffer,但這個 API 不是隨時都能用。

2018 年 Spectre 和 Meltdown 漏洞公開後,瀏覽器為了防範旁路攻擊(side-channel attack),預設停用了 SharedArrayBuffer。網頁要先進入 cross-origin isolated 狀態,瀏覽器才會重新開放。做法是在回應加上這兩個 header:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

少了這兩個 header,WebContainer.boot() 會直接拋出錯誤。我部署 demo 到 Vercel 時就遇到這個問題,本機的 Vite 有設定,部署環境卻忘了加。

要注意的是,設定 COEP 後,所有跨來源的子資源,像是圖片、字型、script,都必須在回應中帶上 Cross-Origin-Resource-Policy: cross-origin,否則會被擋下來。沒有這個 header 的 CDN 資源都會載入失敗。

COOP、COEP 和 CORP 的來龍去脈,在〈從 CORS 淺談瀏覽器安全模型〉有更多說明。

可以用在哪裡

凡是需要隔離、免安裝的 Node.js 執行環境,都可以考慮 WebContainers:

  • 線上 IDE:不需要後端,每個分頁都是獨立的執行環境。
  • 互動式文件:在文件裡嵌入可以執行的範例,讀者不用離開頁面就能修改、執行。
  • 程式碼沙盒:程式競賽、技術面試,支援真正的 npm。
  • 可分享的 demo:一個網址就是一個正在執行的應用程式,不需要部署流程,也沒有伺服器費用。
  • AI coding agent:讓 LLM 在沙盒裡寫程式、執行、再修改。
  • 瀏覽器端的 CI:在瀏覽器裡跑測試和建置,不用排隊等 CI 伺服器。

參考資料