WebAssembly AI 插件项目回顾:理想很丰满,浏览器兼容性很骨感的现实
一、为什么选择 WASM + Rust 做 AI 插件
初始想法很简单:用户在 Figma 或 VS Code Web 里选中一段文字,按快捷键,本地 AI 直接给出改写建议。全程离线,不经过任何服务器,隐私保护拉满。
技术栈选型很自然:Rust 写的推理引擎编译到 WebAssembly,运行在浏览器里。理想中的架构:
第一步很顺利——用 wasm-pack 把一个 200 行的 Rust 示例编译成 .wasm,在浏览器里成功运行了。
// ============================================================
// 第一个 WASM 测试:文本处理功能(编译成功!)
// ============================================================
use wasm_bindgen::prelude::*;
/// 暴露给 JavaScript 的文本预处理函数
/// 在 WASM 中运行,负责清洗和标准化用户输入的文本
#[wasm_bindgen]
pub fn preprocess_text(input: &str) -> String {
// 去掉多余空白字符,统一换行符
input
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\\n")
}
/// 简单的 token 计数,用于评估文本长度是否超过模型限制
#[wasm_bindgen]
pub fn count_tokens(text: &str) -> usize {
// 简化版:按空格分词(实际应该用 tokenizer)
text.split_whitespace().count()
}
JS 调用端也很简洁:
import init, { preprocess_text, count_tokens } from './pkg/my_wasm.js';
await init();
const result = preprocess_text(" hello world \\n rust ");
console.log(result); // "hello world\\nrust"
到这里,我甚至觉得"WASM 项目真简单,两周搞定"。
二、第一道墙:WASM 内存限制
真正的噩梦从加载模型文件开始。一个最轻量的 ONNX 推理模型(比如 tiny-bert)也要 15MB。在 WASM 里,内存默认限制是 4GB——听起来很大,但浏览器的限制远不止这个:
实际测试:15MB 的模型加载后占用约 60MB WASM 堆内存,推理一个 256 token 的文本时峰值内存跳到 180MB。在 Chrome 里还能接受,但在移动端 Safari 上直接在 tab 崩溃。
三、第二道墙:浏览器兼容性地狱
WASM 本身兼容性还可以,但多线程和 SIMD 是两个巨大的坑:
| WASM 基础 | ✅ | ✅ | ✅ | 全部 |
| WASM 线程 | ✅ | ✅ | ⚠️ 需要 COOP/COEP | wasm-bindgen-rayon |
| WASM SIMD | ✅ | ✅ | ❌ 不支持 | 推理加速 |
| SharedArrayBuffer | ✅ | ✅ | ⚠️ 需要特殊 header | 线程间共享模型 |
最关键的问题是 SharedArrayBuffer。要在 WASM 中启用多线程,必须用到它,但 Safari 要求服务端设置特定的 HTTP header:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
作为浏览器插件,我们没有能力控制目标网站的 HTTP header。这意味着:多线程 WASM 在 Safari 上直接无法使用,只能退回到单线程模式,推理速度慢了 3~4 倍。
// ============================================================
// 条件编译:根据平台决定使用多线程还是单线程
// ============================================================
#[cfg(all(target_feature = "atomics", feature = "threads"))]
mod inference {
// 多线程推理实现:利用 wasm-bindgen-rayon + SharedArrayBuffer
pub fn run_parallel(model: &Model, input: &[f32]) -> Vec<f32> {
use rayon::prelude::*;
// 并行计算各层的注意力分数
input.par_chunks(64).map(|chunk| {
model.forward(chunk)
}).flatten().collect()
}
}
#[cfg(not(all(target_feature = "atomics", feature = "threads")))]
mod inference {
// 单线程回退方案:虽然慢但兼容性更好
pub fn run_parallel(model: &Model, input: &[f32]) -> Vec<f32> {
input.chunks(64).map(|chunk| {
model.forward(chunk)
}).flatten().collect()
}
}
四、重新思考:WASM AI 的正确打开方式
经过两周的碰壁,我重新审视了这个项目的可行性。结论是:纯浏览器端的 AI 推理在现阶段还不成熟,但有一些可行的折中方案:
我们最终选择了路径 1——WASM 预处理 + 服务端推理。用户文本在浏览器端完成清洗和 tokenization,只把 token ID 发到服务端,服务端不存原始文本。这是目前技术条件下最务实的方案。
隔了半年再看,这个折中方案确实是最稳的选择——最近 WebGPU 在 Firefox 上的支持已经 beta 了,可能再等一年就能切到纯浏览器端。
五、总结
WASM AI 插件项目是一次"理想照进现实"的典型经历:
这次项目的代码我没扔掉——WASM 文本预处理模块被复用到另一个项目中,算是没白忙活。如果你也在考虑用 WASM 做 AI 推理,希望我的经历能让你少走点弯路。


![pip install安装markitdown时出现ERROR: ‘packages/markitdown[all]‘ is not a valid editable requirement解决方案-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260823004656-6a8a430045592-220x150.png)