## 先说结论

WebAssembly，通常缩写为 Wasm，是一种可以在浏览器和其他运行环境中高效执行的二进制指令格式。它不是一门专门给人手写的高级语言，也不是 JavaScript 的替代品，更像是不同编程语言都可以编译到的一种“通用机器码”。

如果把浏览器比作一座城市，JavaScript 是本地居民最常用的语言，Wasm 则像一套标准集装箱：C、C++、Rust、Python 等语言只要能把程序装进这种集装箱，就有机会被浏览器安全地装载和执行。

本文会依次讲清四件事：

1. Wasm 到底是什么；
2. 它适合解决什么问题；
3. 如何用 Python 快速获得一个基于 Wasm 的程序；
4. 如何把它接入浏览器，做成一个真正可交互的小应用。

## 什么是 WebAssembly

[WebAssembly 官方网站](https://webassembly.org/)给出的定义很准确：Wasm 是一种面向栈式虚拟机的二进制指令格式，也是编程语言的可移植编译目标，可以用于 Web 客户端、服务端以及其他环境。

这句话里有三个关键词。

### 它是一种二进制格式

浏览器实际加载的 Wasm 文件通常以 `.wasm` 结尾，内容是紧凑的二进制指令。它更适合机器快速下载、验证和执行，而不是让开发者直接阅读。

Wasm 也有对应的文本格式 WAT，通常以 `.wat` 结尾，方便调试、学习和工具展示。二者的关系有点像“二进制程序”和“可读的汇编表示”。

### 它是一种编译目标

开发者通常不会直接写 Wasm 二进制，而是先写自己熟悉的语言，再通过工具链编译或移植到 Wasm。

```text
Rust / C / C++ / Python / 其他语言
                  ↓
             编译或移植
                  ↓
             WebAssembly
                  ↓
       浏览器、服务端或其他运行时
```

因此，Wasm 更像 JVM 字节码或一种跨平台机器码，而不是一门和 Python、JavaScript 平级的业务开发语言。

### 它运行在受约束的环境中

Wasm 模块不能默认随意读取本机文件、打开网络连接或操作页面。它需要运行环境显式提供可调用能力。

在浏览器里，Wasm 仍然受到同源策略和浏览器权限模型约束。它可以通过 JavaScript 与 Web API 交互，但不能因为自己是二进制代码就绕过浏览器安全规则。

沙箱提高了隔离性，但不代表加载任何 Wasm 都绝对安全。应用仍要防范恶意计算、资源耗尽、供应链污染以及错误暴露的宿主能力。

## Wasm 和 JavaScript 是什么关系

Wasm 与 JavaScript 更适合协作，而不是互相替代。

| 工作 | 通常由谁负责 |
| --- | --- |
| 页面结构、DOM 与事件 | JavaScript |
| 网络请求和浏览器 API | JavaScript，或由它桥接给 Wasm |
| 大量数值计算、编解码 | Wasm 很有优势 |
| 原生库移植 | Wasm |
| 普通表单和轻量交互 | JavaScript 通常更简单 |

浏览器中的典型流程是：JavaScript 下载并实例化 Wasm 模块，为它提供必要的导入函数，再调用它导出的函数。Wasm 完成计算后，把结果交回 JavaScript，由 JavaScript 更新页面。

WebAssembly 官方规范也单独定义了 JavaScript API 和 Web API：前者负责验证、编译、实例化以及模块的导入导出，后者增加了浏览器中的流式编译和实例化能力。可参考 [WebAssembly Specifications](https://webassembly.org/specs/)。

## Wasm 的作用是什么

Wasm 的价值不是“所有程序都能变快”，而是让原本不容易进入浏览器的代码和计算任务，多了一条标准化运行路径。

### 把已有原生代码带进浏览器

许多成熟项目使用 C、C++ 或 Rust 编写，例如图像处理、音视频编解码、压缩、数据库和游戏引擎。将这些代码编译成 Wasm，可以复用大量既有实现，而不是从头用 JavaScript 重写。

典型场景包括：

- 浏览器中的图片编辑与格式转换；
- 音频、视频编解码；
- CAD、3D、游戏和物理模拟；
- PDF、压缩包和二进制文件处理；
- SQLite 等本地数据能力；
- 密码学和高强度数值计算。

### 在前端执行较重的计算

当计算可以在用户设备完成时，Wasm 能减少服务器往返，把部分工作从云端移到本地。例如离线数据分析、文件预处理和模型推理。

这可能带来更低的服务器成本、更好的离线体验，以及“原始文件不用上传服务器”的隐私优势。但是否真的更快，仍取决于算法、数据规模、浏览器、编译选项和 JavaScript 与 Wasm 之间的数据交换成本。

### 提供跨语言的运行目标

团队可以继续使用擅长某类任务的语言，再把结果部署到支持 Wasm 的运行环境。浏览器之外，Wasm 也可以配合 WASI 等接口运行在服务端、边缘节点或插件系统中。

这让 Wasm 具备一种很有吸引力的产品能力：同一套核心逻辑可以在多种宿主环境复用，而宿主只开放它真正需要的权限。

### 构建安全边界更清楚的插件

插件代码通常来自不同团队甚至第三方。把插件编译为 Wasm，并只向它暴露有限的导入接口，可以比直接加载任意本机动态库更容易控制边界。

但权限设计仍然是宿主程序的责任。如果宿主把危险文件操作或无限制网络能力暴露给模块，沙箱也无法替产品做出正确授权判断。

## 哪些情况不适合用 Wasm

不要因为 Wasm 听起来接近机器码，就把所有前端逻辑都迁过去。以下任务通常没有必要：

- 普通表单校验和 DOM 操作；
- 数据量很小的一次性计算；
- 主要耗时来自网络而非 CPU 的功能；
- 团队没有对应工具链维护经验的短期项目；
- Wasm 运行时和依赖下载成本大于计算收益的页面。

Wasm 还存在启动下载、内存占用、调试体验和语言运行时体积等成本。尤其是 Python 这类动态语言，需要把解释器或运行时一同带进浏览器，首屏成本会明显高于一个简单 JavaScript 函数。

## 用 Python 写 Wasm，准确来说有两条路

“用 Python 写一个 Wasm”可能表达两种不同需求。

### 把 Python 运行时带进浏览器

这是最容易上手的路线，也是本文采用的方式。Pyodide 把 CPython 及一批科学计算生态移植到 WebAssembly/Emscripten，让浏览器可以执行 Python。

这条路线中，真正的 `.wasm` 核心是 Python 运行时。业务 Python 代码由这个运行时解释执行，而不是每个 Python 函数单独编译成一个小型 `.wasm` 文件。

优点是兼容日常 Python 语法、开发快、适合教学和数据类工具；代价是运行时较大，初始化比原生 JavaScript 慢。

### 把程序编译成独立 Wasm 模块

如果目标是极小体积、明确的导入导出接口，或者直接用 `WebAssembly.instantiateStreaming()` 加载独立模块，Rust、C/C++ 和 AssemblyScript 往往拥有更成熟直接的体验。

WebAssembly 官方开发者指南也把 Pyodide、Nuitka 的 `py2wasm` 等列为 Python 路线。不过不同工具对 Python 特性、第三方扩展、浏览器接口和产物格式的支持不同，选择前必须验证目标程序，而不能假设任意 Python 项目都能无修改编译。

对于“先让 Python 在浏览器跑起来”这个目标，Pyodide 是更通俗、稳定的起点。

## 第一个 Python Wasm 示例

先创建目录：

```bash
mkdir python-wasm-demo
cd python-wasm-demo
```

创建 `index.html`：

```html
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Python Wasm Demo</title>
</head>
<body>
    <p id="status">正在加载 Python 运行时...</p>
    <button id="run" type="button" disabled>运行 Python</button>
    <pre id="output"></pre>

    <script src="https://cdn.jsdelivr.net/pyodide/v314.0.4/full/pyodide.js"></script>
    <script>
        const status = document.querySelector("#status");
        const runButton = document.querySelector("#run");
        const output = document.querySelector("#output");

        let pyodide;

        async function initialize() {
            try {
                pyodide = await loadPyodide();
                status.textContent = "Python 运行时已就绪";
                runButton.disabled = false;
            } catch (error) {
                status.textContent = "加载失败";
                output.textContent = String(error);
            }
        }

        runButton.addEventListener("click", () => {
            const result = pyodide.runPython(`
values = [3, 5, 8, 13, 21]
sum(x * x for x in values)
            `);
            output.textContent = `计算结果：${result}`;
        });

        initialize();
    </script>
</body>
</html>
```

然后启动一个本地 HTTP 服务：

```bash
python3 -m http.server 8000
```

打开：

```text
http://localhost:8000
```

点击按钮后，页面会显示 `计算结果：668`。

根据 [Pyodide 官方快速入门](https://pyodide.org/en/stable/usage/quickstart.html)，`pyodide.js` 提供异步的 `loadPyodide()`，它会初始化 Python 环境；`runPython()` 接收 Python 源码字符串，并把可转换的结果返回给 JavaScript。

这已经是一个真实的 Wasm 浏览器应用：页面加载 Pyodide 的 WebAssembly 运行时，Python 负责计算，JavaScript 负责加载、事件和 DOM 展示。

## 做成一个实用的浏览器小工具

把 Python 代码直接嵌在 JavaScript 字符串中适合最小演示。项目稍大后，最好把 Python 和页面代码分开。

目录结构如下：

```text
python-wasm-demo/
├── index.html
└── main.py
```

创建 `main.py`，实现一个简单文本分析器：

```python
import json
import re
from collections import Counter


def analyze_text(text: str) -> str:
    """分析文本，并返回便于 JavaScript 使用的 JSON 字符串。"""
    lines = text.splitlines()
    ascii_words = re.findall(r"[A-Za-z0-9_]+", text.lower())
    frequencies = Counter(ascii_words).most_common(5)

    result = {
        "characters": len(text),
        "characters_without_spaces": sum(
            1 for character in text if not character.isspace()
        ),
        "lines": len(lines),
        "ascii_words": len(ascii_words),
        "top_ascii_words": frequencies,
    }
    return json.dumps(result, ensure_ascii=False)
```

再把 `index.html` 改成一个完整交互页面：

```html
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>浏览器 Python 文本分析器</title>
    <style>
        body {
            max-width: 760px;
            margin: 48px auto;
            padding: 0 20px;
            font-family: system-ui, sans-serif;
            color: #0f172a;
        }
        textarea, pre {
            box-sizing: border-box;
            width: 100%;
            padding: 14px;
            border: 1px solid #cbd5e1;
            border-radius: 10px;
        }
        textarea { min-height: 180px; }
        pre { min-height: 160px; background: #f8fafc; }
        button { margin: 12px 0; padding: 10px 16px; }
    </style>
</head>
<body>
    <h1>浏览器 Python 文本分析器</h1>
    <p id="status">正在加载 Python 运行时...</p>

    <textarea id="source">WebAssembly lets code run across environments.
Python can run in the browser through Pyodide.</textarea>
    <button id="analyze" type="button" disabled>开始分析</button>
    <pre id="output">等待运行</pre>

    <script src="https://cdn.jsdelivr.net/pyodide/v314.0.4/full/pyodide.js"></script>
    <script>
        const status = document.querySelector("#status");
        const source = document.querySelector("#source");
        const analyzeButton = document.querySelector("#analyze");
        const output = document.querySelector("#output");

        let pyodide;

        async function initialize() {
            try {
                pyodide = await loadPyodide();

                const response = await fetch("./main.py");
                if (!response.ok) {
                    throw new Error(`main.py 加载失败：${response.status}`);
                }

                const pythonSource = await response.text();
                await pyodide.runPythonAsync(pythonSource);

                status.textContent = "Python 运行时已就绪";
                analyzeButton.disabled = false;
            } catch (error) {
                status.textContent = "初始化失败";
                output.textContent = String(error);
            }
        }

        analyzeButton.addEventListener("click", () => {
            try {
                pyodide.globals.set("source_text", source.value);
                const resultJson = pyodide.runPython(
                    "analyze_text(source_text)"
                );
                const result = JSON.parse(resultJson);
                output.textContent = JSON.stringify(result, null, 2);
            } catch (error) {
                output.textContent = String(error);
            }
        });

        initialize();
    </script>
</body>
</html>
```

再次运行本地服务并打开页面：

```bash
python3 -m http.server 8000
```

用户输入文本后，执行链路如下：

```text
用户点击按钮
    ↓
JavaScript 读取 textarea
    ↓
字符串写入 Pyodide 全局变量
    ↓
Wasm 中的 Python 运行时调用 analyze_text
    ↓
Python 返回 JSON 字符串
    ↓
JavaScript 解析结果并更新 pre 元素
```

这个例子故意使用 JSON 字符串作为 Python 与 JavaScript 的边界。简单字符串、数字和布尔值转换直接；复杂 Python 对象可能以代理对象形式暴露，需要理解转换和释放规则。明确的 JSON 契约更容易调试，也适合作为前后两种语言之间的稳定接口。

## 为什么要通过 HTTP 服务打开

直接双击 `index.html`，地址会是 `file://`。浏览器对本地文件读取有限制，`fetch("./main.py")` 很可能失败。

使用 `python3 -m http.server` 后，页面和 `main.py` 通过 `http://localhost:8000` 提供，浏览器会按正常 Web 资源处理它们。

上线时也要正确配置静态资源路径、缓存和跨域策略。Pyodide 官方 FAQ 同样提醒，本地文件与跨域请求会受到浏览器同源策略约束。

## 如何加载 Python 第三方包

Pyodide 初始化完成后，默认提供 Python 标准库。其他包需要额外加载。

如果包已经包含在 Pyodide 分发中，可以从 JavaScript 加载：

```javascript
await pyodide.loadPackage("numpy");

const result = pyodide.runPython(`
import numpy as np
values = np.array([1, 2, 3, 4])
float(values.mean())
`);

console.log(result);
```

对于纯 Python wheel 或 Pyodide 兼容包，通常使用 `micropip`：

```javascript
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install("snowballstemmer");
```

[Pyodide 加载包文档](https://pyodide.org/en/stable/usage/loading-packages.html)指出，纯 Python wheel 通常更容易使用；带有 C、Rust 等本机扩展的包必须有兼容的 `wasm32/emscripten` 构建，不能认为所有能被桌面版 `pip` 安装的包都能在浏览器工作。

每增加一个包，还会增加下载体积和初始化时间。实际产品应只加载当前页面真正需要的依赖，并配置长期缓存。

## 耗时计算要放进 Web Worker

最小示例在浏览器主线程运行 Python。短计算没有问题，但耗时任务会阻塞按钮、滚动和动画，让页面看起来“卡死”。

生产应用应把长时间 Python 计算移到 Web Worker：

```text
主线程：界面、点击、进度、结果展示
                     ↕ postMessage
Worker：加载 Pyodide、执行 Python、返回结果
```

当前 Pyodide 官方文档要求使用模块类型 Worker，因为它依赖 ES 模块形式的运行文件。创建方式类似：

```javascript
const worker = new Worker("./worker.js", { type: "module" });

worker.postMessage({
    id: "task-1",
    text: "需要分析的文本",
});

worker.addEventListener("message", (event) => {
    console.log(event.data);
});
```

Worker 不能直接操作 DOM，只能通过消息与主线程交换数据。这反而能形成更清楚的边界：Worker 负责计算，主线程负责界面。

完整模式可参考 [Pyodide Web Worker 官方文档](https://pyodide.org/en/stable/usage/webworker.html)。

## 浏览器接入 Wasm 的通用方式

如果使用 Rust 或 C 编译出独立的 `math.wasm`，不经过 Pyodide，浏览器通常通过 JavaScript 原生 API 加载：

```javascript
const { instance } = await WebAssembly.instantiateStreaming(
    fetch("./math.wasm"),
    {}
);

const result = instance.exports.add(20, 22);
console.log(result);
```

这个例子展示了 Wasm 接入浏览器的核心结构：

- `fetch()` 下载二进制模块；
- `instantiateStreaming()` 边接收边编译并实例化；
- 第二个参数提供模块需要的导入对象；
- `instance.exports` 暴露模块导出的函数、内存等能力。

服务器还应为 `.wasm` 返回正确的 `Content-Type: application/wasm`，否则流式实例化可能失败。工具链通常会生成一层 JavaScript 胶水代码，帮助处理字符串、数组、内存和异步接口。

Pyodide 把这些底层加载和语言运行时细节封装在 `loadPyodide()` 后面，所以 Python 示例看起来更像日常 Web 开发。

## 从 Demo 走向产品要注意什么

### 控制首屏成本

Python 运行时和科学计算包可能比普通前端脚本大得多。不要在用户还没进入相关功能时就全部加载。

可以采用：

- 用户进入工具页后再懒加载；
- 显示真实的初始化状态；
- 使用固定版本和长期缓存；
- 只加载需要的包；
- 对常用资源使用 CDN 或自行托管。

### 固定依赖版本

演示中使用了明确版本 `v314.0.4`。生产环境不要使用 `dev` 或不固定版本的地址，否则上游更新可能造成行为改变、缓存失效或兼容问题。

升级时应测试 Python 版本、包 ABI、浏览器支持和产物大小，而不是只替换 CDN URL。

### 减少跨边界调用

JavaScript 与 Python/Wasm 可以互相调用，但频繁传递大量对象可能抵消计算收益。

更好的方式是一次传入一批数据，在 Wasm 内完成一段完整计算，再一次返回结构化结果。把循环放在 Wasm 内部，通常比让 JavaScript 每次循环都跨边界调用更合理。

### 不要把浏览器当完整操作系统

桌面 Python 程序常依赖文件系统、子进程、原始 Socket、系统线程或本机动态库。浏览器环境不会原样提供这些能力。

网络访问要遵守 Fetch 和 CORS；文件访问要经过用户选择或浏览器 API；不兼容的本机扩展需要专门移植。评估项目时，应先列出依赖的系统能力，再判断能否在浏览器中替代。

### 不要运行不受信任的无限代码

Wasm 有内存安全和沙箱边界，但用户提交的 Python 仍可能死循环、占满 CPU 或申请大量内存。在线代码执行器需要 Worker 隔离、超时、中止机制、资源限制和输入校验。

沙箱解决的是能力边界，不自动解决拒绝服务和业务滥用。

## 常见误解

**Wasm 一定比 JavaScript 快**：不一定。算法、运行时、数据交换和启动成本都会影响结果，必须用真实负载测试。

**Wasm 可以直接操作 DOM**：通常需要通过 JavaScript 或宿主提供的接口。Wasm 本身不内置 DOM。

**Python 文件被直接编译成了小型 wasm**：本文的 Pyodide 路线是由 Wasm 版 CPython 执行 Python 代码，业务脚本并不是独立 Wasm 模块。

**所有 Python 包都能在 Pyodide 安装**：纯 Python 包通常更容易；本机扩展必须有兼容的 Emscripten/Wasm 构建。

**有沙箱就不用考虑安全**：错误暴露的宿主接口、资源耗尽和供应链风险仍然存在。

**打开本地 HTML 就能工作**：页面通过 `fetch()` 加载 Python 或 Wasm 文件时，应使用本地 HTTP 服务，避免 `file://` 限制。

## 如何判断项目是否值得用 Wasm

在引入 Wasm 前，可以回答五个问题：

1. 是否有必须复用的非 JavaScript 代码或生态？
2. 性能瓶颈是否真的是 CPU 计算，而不是网络或 DOM？
3. 计算是否足够大，能覆盖运行时启动和语言边界成本？
4. 所需依赖是否支持浏览器 Wasm 环境？
5. 团队是否能维护构建、调试、缓存和升级链路？

如果只是几十行普通页面逻辑，JavaScript 往往更直接。如果要把成熟算法库、Python 数据能力或重型计算带到浏览器，Wasm 才开始显示真正价值。

## 总结

Wasm 是一种安全、紧凑、可移植的二进制执行格式，也是多种语言进入浏览器和其他运行时的共同编译目标。它最重要的作用不是取代 JavaScript，而是扩展 Web 能执行的语言、库和计算类型。

用 Python 快速体验 Wasm，最合适的入门方式之一是 Pyodide：

1. 浏览器加载 Wasm 版 CPython；
2. JavaScript 用 `runPython()` 或 `runPythonAsync()` 执行 Python；
3. 两种语言通过明确的数据接口交换输入输出；
4. 页面仍由 JavaScript 管理；
5. 重计算放进 Web Worker，避免阻塞界面。

当你能清楚区分“Wasm 运行时”“Python 业务代码”和“JavaScript 宿主”这三层，就已经掌握了把 Python Wasm 应用到浏览器的基本架构。

## 参考资料

- [WebAssembly 官方网站](https://webassembly.org/)
- [WebAssembly Specifications](https://webassembly.org/specs/)
- [WebAssembly Developers Guide](https://webassembly.org/getting-started/developers-guide/)
- [Pyodide 官方快速入门](https://pyodide.org/en/stable/usage/quickstart.html)
- [Pyodide 加载第三方包](https://pyodide.org/en/stable/usage/loading-packages.html)
- [Pyodide Web Worker](https://pyodide.org/en/stable/usage/webworker.html)
