导语:浏览器成为多语言运行时的第二现场
浏览器只有一个 JavaScript 运行时,但用户想把 Python、Ruby、Lua、SQL 都搬进网页——用于教学、数据可视化、沙箱执行甚至离线计算。WASM 让这一切成为现实:把解释器整个编译成 .wasm,浏览器就成了任意语言的运行时宿主。本文系统讲这套生态:Pyodide、Ruby WASI、QuickJS、Wasmtime 嵌入,跨语言调用与对象桥接,性能与体积的权衡,以及教学/沙箱/客户端计算三大场景与选型矩阵。
前置:/wasm-javascript-interop/(JS 互操作)、/wasm-embedding-hosting-apis/(嵌入 API)、/wasm-wasmtime-runtime/(Wasmtime 运行时)。
目录
- 1. 为什么在浏览器跑一门语言:场景与动机
- 2. Pyodide:浏览器里的 Python 全栈
- 3. Ruby WASI 与 QuickJS:脚本语言的 WASM 化
- 4. Wasmtime 嵌入与自建运行时
- 5. 跨语言调用与对象桥接
- 6. 性能与体积:解释器进 WASM 的代价
- 7. 场景一:教学与在线 IDE
- 8. 场景二:沙箱与客户端计算
- 9. 运行时选型矩阵
- 10. 速查表与一句话记忆
- 延伸阅读
1. 为什么在浏览器跑一门语言:场景与动机
在线教学/IDE 要免安装跑 Python、沙箱要执行不可信代码、客户端计算要让数据不出设备、还要复用已有 Python/R 生态。四条路线只有一条走得通:
□ 转译(Python→JS):运行时行为有偏差、调试困难、生态断裂
□ 编译官方解释器进 WASM:原生态(CPython/CRuby 原实现)、零重写
□ 后端代跑:多一次网络往返、数据出域、有冷启动
□ 本地安装:要求用户装环境,体验割裂
→ WASM = 浏览器里「真跑这门语言」,其余方案都是妥协
一句话总结:WASM 语言运行时 = 把官方解释器整个搬进浏览器,教学、沙箱、客户端计算、生态复用四类场景共享同一套机制。
2. Pyodide:浏览器里的 Python 全栈
Pyodide 把 CPython 3 编译为 WASM,内置 numpy/pandas/scipy/matplotlib 科学计算栈,还有 micropip 从 PyPI 安装纯 Python 包。
import { loadPyodide } from 'pyodide';
const pyodide = await loadPyodide({
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.26.2/full/',
});
const result = await pyodide.runPythonAsync(`
import numpy as np
a = np.array([1, 2, 3])
(a * 2).tolist()
`);
console.log(result); // [2, 4, 6]
await pyodide.runPythonAsync(`
import micropip
await micropip.install('requests')
# scikit-learn / pandas 等已预编译包亦可安装
`);
Pyodide 的关键机制:Python 堆内存运行在 WASM 线性内存里,Python 对象通过 PyProxy 暴露给 JS;numpy 数组可零拷贝共享为 JS TypedArray。
一句话总结:Pyodide = 浏览器里的 CPython + 科学计算栈 + PyPI,Python 代码原样运行,数据通过 PyProxy 与 JS 互通。
3. Ruby WASI 与 QuickJS:脚本语言的 WASM 化
3.1 Ruby WASI(ruby.wasm)
ruby.wasm 项目把 CRuby 编译为 WASI 目标,支持解释执行与预编译字节码:
<script type="module">
import { DefaultRubyVM } from 'https://cdn.jsdelivr.net/npm/ruby-wasm-wasi@2.3.0';
const vm = await DefaultRubyVM();
vm.eval('puts "Hello from Ruby in WASM"');
</script>
命令行构建:ruby.wasm build --ruby-version 3.2 src.rb -o ruby-app.wasm,产物可直接 wasmtime run 或在浏览器加载。
3.2 QuickJS:微型 JS 引擎
QuickJS 是 Bellard 写的迷你 JS 引擎,编译成 WASM 后常用于「页面内嵌 JS 沙箱」:
import { getQuickJS } from 'quickjs-emscripten';
const { QuickJS, shouldInterruptAfterDeadline } = await getQuickJS();
const vm = QuickJS.createVm();
vm.setInterruptHandler(shouldInterruptAfterDeadline(1000)); // 1s 超时
const res = vm.evalCode('1 + 2');
console.log(res.consume(vm.dump)); // 3
QuickJS WASM 约 1~2MiB(gzip 后几百 KB),远小于完整引擎,适合「可信边界内再跑一个小 JS 世界」。
一句话总结:Ruby WASI 搬 CRuby、QuickJS 搬微型 JS 引擎——解释器进 WASM 不是 Python 专利,任何语言编译到 wasm32 都能在浏览器跑。
4. Wasmtime 嵌入与自建运行时
不想用现成方案时,把 Wasmtime 作为库嵌入,自己控制 WASI 能力与模块生命周期:
use wasmtime::{Engine, Module, Store, WasiCtx, WasiCtxBuilder};
let engine = Engine::default();
let wasi = WasiCtxBuilder::new()
.inherit_stdio()
.env("ALLOW", "1") // 显式注入环境变量
.build();
let mut store = Store::new(&engine, wasi);
let module = Module::from_file(&engine, "app.wasm")?;
// 绑定 wasi 到 linker 后实例化,调用 _start 入口
自建的价值:按需裁剪能力(无文件系统、仅白名单网络)、批量实例化做多租户、把 WASM 模块嵌进 Electron/桌面/服务端同一套代码。
一句话总结:Wasmtime 嵌入 = 自己掌握 WASI 能力注入与模块生命周期,适合需要精细裁剪权限、批量实例化或跨端复用的场景。
5. 跨语言调用与对象桥接
5.1 JS ⇄ Python 双向调用
// JS 调用 Python 函数
const pyFunc = pyodide.globals.get('add');
console.log(pyFunc(1, 2)); // 3
// Python 调用 JS 对象
await pyodide.runPythonAsync(`
from js import document
document.title = "被 Python 改了"
`);
5.2 三种桥接形态与性能要点
□ PyProxy(pyodide):Python 对象 ↔ JS 引用,.get/.set/.call
□ JSProxy:JS 对象在 Python 侧变为可调用代理
□ TypedArray 零拷贝:numpy 数组 ↔ Float32Array 共享底层 buffer
→ 批量数据走 TypedArray 共享内存,避免逐元素过桥
→ 对象句柄用完 .destroy(),防止内存泄漏
一句话总结:跨语言桥接 = 代理对象 + 共享内存——小数据走 Proxy 调用、大数据走 TypedArray 零拷贝,边界转换是性能分水岭。
6. 性能与体积:解释器进 WASM 的代价
| 运行时 | 体积(gzip 后) | 速度 | 启动 |
|---|---|---|---|
| Pyodide(含科学栈) | 10~12MiB | CPython 原速 ~1x | 200ms~1s |
| Ruby WASI | 5~8MiB | CRuby 原速 ~1x | 100~500ms |
| QuickJS WASM | 0.5~1MiB | 原生 ~1x | <50ms |
关键认知:WASM 不拖慢解释器——CPython/CRuby 本身是解释执行,编译成 WASM 后速度与原生基本持平(纯计算场景 SIMD 甚至略胜)。真正的代价是体积与启动:解释器 + 标准库 + 依赖包都要下载加载。
优化手段:
□ 按需裁剪标准库 / 用 esbuild 摇树 JS 胶水
□ 预编译字节码(Ruby)跳过 parse 阶段
□ 模块实例复用 + 预加载到 Cache API
□ 懒加载:用户真正触发执行时再 fetch 解释器
一句话总结:解释器进 WASM 性能不降、体积和启动才要命——运行时体积是选型第一考量,懒加载与 Cache API 是标配优化。
7. 场景一:教学与在线 IDE
典型架构是「编辑器 + WASM 解释器 + 输出面板」,全部页面内完成、零后端:
<script type="module">
import { loadPyodide } from 'pyodide';
const pyodide = await loadPyodide();
pyodide.setStdout({ batched: (s) => outputPanel.write(s) }); // 捕获 stdout
editor.on('run', async () => {
await pyodide.runPythonAsync(editor.getValue());
});
</script>
教学场景三个注意点:stdout/stderr 捕获(setStdout/setStderr 重定向到输出面板)、执行超时(用 setInterruptBuffer 配合 worker 中断死循环)、状态隔离(每次运行是否保留全局变量,教学通常不保留)。
一句话总结:在线 IDE = 编辑器 + WASM 解释器 + stdout 重定向,教学场景要格外处理超时中断与全局状态隔离。
8. 场景二:沙箱与客户端计算
代码沙箱:不可信代码在 WASM 沙箱里执行,物理上无法访问文件系统、网络(除非显式注入)与 DOM;WASM 无 syscall、无进程、内存越界 trap,天然比 eval 安全——仍需自设超时与内存水位防 DoS:
const result = vm.evalCode(userCode, { type: 'global' }); // QuickJS,已设 1s 中断
// Pyodide 跑不可信 Python:不注入任何 WASI 能力,默认零权限
客户端计算:数据不出设备,隐私 + 离线双收,典型如 CSV 解析 + pandas 聚合 + 图表:
const df = await pyodide.runPythonAsync(`
import pandas as pd, io
df = pd.read_csv(io.StringIO(csvText))
df.groupby('region')['amount'].sum().to_dict()
`);
一句话总结:沙箱用 WASM 隔离边界 + 自设超时,客户端计算让数据不离开设备——两类场景都吃「本地执行」的红利。
9. 运行时选型矩阵
| 需求 | 选择 |
|---|---|
| 浏览器跑 Python + 科学栈 | Pyodide |
| 浏览器跑 Ruby | ruby.wasm(DefaultRubyVM) |
| 页面内嵌小 JS 沙箱 | QuickJS(quickjs-emscripten) |
| 自建/裁剪运行时 | Wasmtime 嵌入 + 自定义 WASI |
| 多语言统一平台 | Wasmer(多语言绑定) |
| 只执行轻量脚本 | QuickJS(避免重解释器体积) |
选型决策树:
□ 语言有官方 WASM 构建(pyodide/ruby.wasm)?→ 直接用
□ 无官方构建且体积敏感?→ QuickJS 或换语言
□ 需要精细权限控制?→ Wasmtime 自建
□ 科学计算?→ Pyodide(numpy/pandas 已内置)
一句话总结:选型 = 语言生态(有无官方 WASM)+ 体积预算 + 权限需求——有官方构建直接用、体积敏感降级小引擎、权限敏感自建 Wasmtime。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| Python 浏览器运行时 | Pyodide(CPython + 科学栈 + micropip) |
| Ruby 浏览器运行时 | ruby.wasm / DefaultRubyVM |
| 小体积 JS 沙箱 | QuickJS WASM(~1MiB) |
| 自建运行时 | Wasmtime 嵌入 + 自定义 WASI |
| 跨语言调用 | PyProxy/JSProxy + TypedArray 零拷贝 |
| 性能 | WASM 不拖慢解释器,代价在体积与启动 |
| 体积优化 | 裁剪标准库 + 懒加载 + Cache API |
| 教学场景 | stdout 重定向 + 超时中断 + 状态隔离 |
| 沙箱场景 | WASM 边界 + 自设超时与内存水位 |
| 客户端计算 | 数据不出设备,隐私 + 离线 |
一句话记忆:WASM 语言运行时 = 把官方解释器编译成 .wasm 塞进浏览器——Python 用 Pyodide(科学栈 + micropip)、Ruby 用 ruby.wasm、小沙箱用 QuickJS、要裁剪权限就 Wasmtime 自建;跨语言靠 Proxy 代理 + TypedArray 零拷贝;WASM 不拖慢解释器、真正代价是体积与启动(懒加载 + Cache API);教学盯 stdout/超时,沙箱盯隔离边界——「原生态真跑,别转译假装」是核心心法。
延伸阅读
- /wasm-javascript-interop/ — JS 互操作与对象桥接
- /wasm-embedding-hosting-apis/ — 嵌入与托管 API
- /wasm-wasmtime-runtime/ — Wasmtime 运行时详解
- /wasm-ai-inference-browser/ — 浏览器端机器学习
- /wasm-component-model-wit/ — 组件模型与跨语言互操作
- [[ai-ml]] — AI 推理与浏览器 ML
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。