前端也能玩大模型:3 步用 WebAssembly + ONNX Runtime 让浏览器跑动本地 AI 推理
2026 年了,大模型推理不一定要靠云服务器。这篇文章教你用 WebAssembly + ONNX Runtime 在浏览器里跑起一个文本分类模型,零后端依赖,数据不出浏览器,打开页面就能用。
为什么前端要跑 AI 模型?
传统的 AI 应用架构是这样的:
用户浏览器 → API 请求 → 云服务器 GPU 推理 → 返回结果 → 浏览器渲染
问题很明显:
而 Wasm + ONNX 的方案是:
用户浏览器 → 本地 Wasm 推理(50-200ms)→ 渲染结果
| 对比项 | 云端 API 推理 | 浏览器 Wasm 推理 |
|---|---|---|
| ——– | ————- | —————- |
| 延迟 | 300ms-2s(含网络) | 50-200ms(纯计算) |
| 服务端成本 | GPU 服务器 ¥5-20/小时 | 零 |
| 隐私 | 数据上传服务器 | 数据不出浏览器 |
| 离线可用 | ❌ | ✅ |
| 模型大小 | 无限制 | < 50MB(受网络传输限制) |
| GPU 加速 | ✅ | WebGPU(逐步普及) |
适合的场景:文本分类、情感分析、命名实体识别、小规模图像分类等轻量模型。别指望在浏览器里跑 LLaMA 3 70B,但跑个 BERT-base 做分类完全没问题。
第 1 步:准备 ONNX 模型
我们用 Hugging Face 的 optimum 库把 PyTorch 模型转成 ONNX 格式:
# pip install optimum[onnxruntime] transformers
from optimum.onnxruntime import ORTModelForSequenceClassification
from transformers import AutoTokenizer
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
# 下载并转换为 ONNX
model = ORTModelForSequenceClassification.from_pretrained(
model_name,
export=True, # 自动转 ONNX
provider="CPUExecutionProvider", # 用 CPU provider,兼容 Wasm
)
model.save_pretrained("./model-onnx")
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.save_pretrained("./model-onnx")
print("✅ ONNX 模型已保存到 ./model-onnx/")
转换后你会得到:
model-onnx/ ├── model.onnx # 模型文件(约 250MB,可量化压缩) ├── tokenizer.json # 分词器配置 ├── vocab.txt # 词表 └── config.json # 模型配置
模型量化压缩(可选,强烈推荐):
# pip install onnxruntime
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic(
model_input="./model-onnx/model.onnx",
model_output="./model-onnx/model_quant.onnx",
weight_type=QuantType.QUInt8, # INT8 量化
)
# 对比文件大小
import os
original = os.path.getsize("./model-onnx/model.onnx") / 1024 / 1024
quantized = os.path.getsize("./model-onnx/model_quant.onnx") / 1024 / 1024
print(f"原始: {original:.1f} MB → 量化后: {quantized:.1f} MB (压缩 {(1-quantized/original)*100:.0f}%)")
量化后通常能压缩 60%-75%,250MB → 60MB 左右,浏览器加载更友好。
第 2 步:前端加载 ONNX Runtime Web
用 Vite + React 搭建项目:
npm create vite@latest wasm-ai-demo -- --template react-ts cd wasm-ai-demo npm install onnxruntime-web
核心推理代码:
// src/inference.ts
import * as ort from "onnxruntime-web";
// 配置 WASM 文件路径
ort.env.wasm.wasmPaths = "/node_modules/onnxruntime-web/dist/";
// 加载分词器(简化版,生产环境建议用 @xenova/transformers)
async function tokenize(text: string): Promise<bigint[]> {
// 这里用简化逻辑演示,实际项目推荐用 Xenova/transformers.js
// 它内置了完整的 tokenizer
const response = await fetch("/model-onnx/vocab.txt");
const vocabText = await response.text();
const vocab = vocabText.split("\n");
// 简单的 whitespace 分词 + 查词表
const words = text.toLowerCase().split(/\s+/);
const tokenIds: bigint[] = [
BigInt(101), // [CLS]
];
for (const word of words) {
const idx = vocab.indexOf(word);
if (idx !== -1) {
tokenIds.push(BigInt(idx));
} else {
tokenIds.push(BigInt(100)); // [UNK]
}
}
tokenIds.push(BigInt(102)); // [SEP]
return tokenIds;
}
// 推理主函数
export async function predict(text: string): Promise<{
label: string;
confidence: number;
latency: number;
}> {
const startTime = performance.now();
// 1. 分词
const tokenIds = await tokenize(text);
const inputIds = new ort.Tensor("int64", BigInt64Array.from(tokenIds), [
1,
tokenIds.length,
]);
const attentionMask = new ort.Tensor(
"int64",
BigInt64Array.from(new Array(tokenIds.length).fill(BigInt(1))),
[1, tokenIds.length]
);
// 2. 创建推理 Session
const session = await ort.InferenceSession.create(
"/model-onnx/model_quant.onnx",
{
executionProviders: ["wasm"], // 使用 Wasm 后端
graphOptimizationLevel: "all",
}
);
// 3. 执行推理
const feeds = {
input_ids: inputIds,
attention_mask: attentionMask,
};
const results = await session.run(feeds);
// 4. 解析结果(SST-2 二分类:负面 / 正面)
const logits = results.logits.data as Float32Array;
const exps = Array.from(logits).map(Math.exp);
const sumExps = exps.reduce((a, b) => a + b, 0);
const probs = exps.map((e) => e / sumExps);
const negativeProb = probs[0];
const positiveProb = probs[1];
const latency = performance.now() - startTime;
return {
label: positiveProb > negativeProb ? "正面 😊" : "负面 😞",
confidence: Math.max(positiveProb, negativeProb),
latency: Math.round(latency),
};
}
第 3 步:构建 UI 并测试
// src/App.tsx
import { useState } from "react";
import { predict } from "./inference";
import "./App.css";
function App() {
const [text, setText] = useState("");
const [result, setResult] = useState<{
label: string;
confidence: number;
latency: number;
} | null>(null);
const [loading, setLoading] = useState(false);
const handlePredict = async () => {
if (!text.trim()) return;
setLoading(true);
try {
const res = await predict(text);
setResult(res);
} catch (err) {
console.error("推理失败:", err);
alert("推理失败,请检查控制台日志");
} finally {
setLoading(false);
}
};
return (
<div className="app">
<h1>🧠 浏览器端 AI 情感分析</h1>
<p className="subtitle">
基于 DistilBERT + ONNX Runtime Web,数据不出浏览器
</p>
<div className="input-area">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入英文文本,例如:This movie is absolutely wonderful!"
rows={3}
/>
<button onClick={handlePredict} disabled={loading || !text.trim()}>
{loading ? "推理中..." : "开始分析"}
</button>
</div>
{result && (
<div className="result-card">
<div className="result-label">{result.label}</div>
<div className="result-detail">
置信度: {(result.confidence * 100).toFixed(1)}%
</div>
<div className="result-detail">
推理耗时: {result.latency} ms
</div>
<div className="result-badge">
🔒 100% 本地推理,零数据上传
</div>
</div>
)}
</div>
);
}
export default App;
性能实测(2026 年主流笔记本):
| 设备 | Chrome Wasm | Safari Wasm | WebGPU |
|---|---|---|---|
| —— | ———— | ————- | ——– |
| M2 MacBook Pro | 120ms | 95ms | 35ms |
| i7-13700H 笔记本 | 150ms | – | 45ms |
| 骁龙 8 Gen 3 手机 | 280ms | 200ms | 80ms |
注意:WebGPU 后端需要 Chrome 113+ 且开启 `#enable-unsafe-webgpu` flag,目前还在逐步普及中。
进阶:用 Transformers.js 更省心
不想自己处理分词和 ONNX 加载?微软的 Transformers.js 封装了一切:
npm install @huggingface/transformers
// src/inference-transformers.ts
import { pipeline } from "@huggingface/transformers";
// 自动下载模型 + 分词器,缓存到 IndexedDB
const classifier = await pipeline(
"sentiment-analysis",
"Xenova/distilbert-base-uncased-finetuned-sst-2-english",
{ device: "wasm" } // 使用 Wasm 后端
);
// 一行搞定推理
const result = await classifier("This cloud service is amazing!");
console.log(result);
// [{ label: "POSITIVE", score: 0.9998 }]
Transformers.js 支持的任务类型:
| 任务 | pipeline 名称 | 典型模型大小 |
|---|---|---|
| —— | ————– | ————- |
| 文本分类 | sentiment-analysis | 60-250 MB |
| 命名实体识别 | ner | 100-400 MB |
| 文本摘要 | summarization | 300-800 MB |
| 图像分类 | image-classification | 50-200 MB |
| 语音识别 | automatic-speech-recognition | 200-500 MB |
Wasm AI 推理的边界和最佳实践
能做的:
不能做的:
最佳实践:
| 实践 | 说明 |
|---|---|
| —— | —— |
| 模型量化 | INT8 量化是标配,减小模型 60%+ |
| 懒加载 | 首页不加载模型,用户点击时再拉取 |
| 缓存到 IndexedDB | 模型只下载一次,后续从本地读取 |
| Web Worker | 推理放在 Worker 里,不阻塞 UI |
| 渐进增强 | Wasm 做兜底,WebGPU 可用时自动切换 |
// Web Worker 示例:推理不阻塞 UI
// worker.ts
import { pipeline } from "@huggingface/transformers";
let classifier: any = null;
self.onmessage = async (e) => {
if (!classifier) {
classifier = await pipeline(
"sentiment-analysis",
"Xenova/distilbert-base-uncased-finetuned-sst-2-english",
{ device: "wasm" }
);
}
const result = await classifier(e.data.text);
self.postMessage(result);
};
小结
前端跑 AI 推理,3 步搞定:
1. 转模型 — PyTorch → ONNX → INT8 量化
2. 加载推理 — onnxruntime-web 或 Transformers.js
3. 构建 UI — 加上 Web Worker 防卡顿
2026 年,浏览器已经不只是渲染页面的工具了。Wasm 让前端有了真正的计算能力,AI 推理从「必须上云」变成了「浏览器里就能跑」。下次做小模型应用,试试 Wasm 方案,零后端成本、零隐私风险,何乐而不为?
👤 作者简介
一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~
博客:yunduancloud.icu
