前端也能玩大模型:3 步用 WebAssembly + ONNX Runtime 让浏览器跑动本地 AI 推理

阿里云上云采购季 2核2G云服务器99元/年 续费同价

2026 年了,大模型推理不一定要靠云服务器。这篇文章教你用 WebAssembly + ONNX Runtime 在浏览器里跑起一个文本分类模型,零后端依赖,数据不出浏览器,打开页面就能用。

为什么前端要跑 AI 模型?

传统的 AI 应用架构是这样的:

用户浏览器 → API 请求 → 云服务器 GPU 推理 → 返回结果 → 浏览器渲染

问题很明显:
– 延迟高:网络往返动辄几百毫秒
– 成本高: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 推理的边界和最佳实践

能做的:
– 轻量级分类/检测任务
– 隐私敏感的本地推理
– 离线 AI 功能
– 快速原型验证

不能做的:
– 大规模生成式模型(LLM > 1B 参数)
– 高吞吐量推理服务
– 需要 CUDA 优化的任务

最佳实践:

实践 说明
模型量化 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前沿动态~

博客:y1cloud.cn

发表评论