先说结论

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

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

本文会依次讲清四件事:

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

什么是 WebAssembly

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

这句话里有三个关键词。

它是一种二进制格式

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

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

它是一种编译目标

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

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

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 示例

先创建目录:

mkdir python-wasm-demo
cd python-wasm-demo

创建 index.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 服务:

python3 -m http.server 8000

打开:

http://localhost:8000

点击按钮后,页面会显示 计算结果:668

根据 Pyodide 官方快速入门pyodide.js 提供异步的 loadPyodide(),它会初始化 Python 环境;runPython() 接收 Python 源码字符串,并把可转换的结果返回给 JavaScript。

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

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

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

目录结构如下:

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

创建 main.py,实现一个简单文本分析器:

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 改成一个完整交互页面:

<!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>

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

python3 -m http.server 8000

用户输入文本后,执行链路如下:

用户点击按钮
    ↓
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 加载:

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

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

Pyodide 加载包文档指出,纯 Python wheel 通常更容易使用;带有 C、Rust 等本机扩展的包必须有兼容的 wasm32/emscripten 构建,不能认为所有能被桌面版 pip 安装的包都能在浏览器工作。

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

耗时计算要放进 Web Worker

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

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

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

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

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 官方文档

浏览器接入 Wasm 的通用方式

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

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 应用到浏览器的基本架构。

参考资料