浏览器端模型推理选型:ONNX Runtime Web 与 Transformers.js 对比
浏览器端模型推理选型ONNX Runtime Web 与 Transformers.js 对比标签AI、WASM AI、推理、ONNX、Transformers.js、浏览器一、在浏览器里跑模型但问题是浏览器里跑模型推理选什么框架两个名字反复出现ONNX Runtime Web 和 Transformers.js。一个是微软维护的推理引擎一个是 Hugging Face 开源的库。我两边都试了试写这篇文章记录对比结果。这篇文章不打算说XX 更好而是把它们的差异摆在桌面上。看完之后你应该能根据自己项目的需求做出选择。二、两者的本质是什么在比较之前需要先搞清楚它们分别是什么。flowchart TB subgraph ONNX[ONNX Runtime Web] O1[微软维护的跨平台推理引擎] O2[输入ONNX 格式的模型文件] O3[后端WebAssemblyCPU/ WebGPU] O4[核心能力高效的张量运算 图优化] O5[模型来源从 PyTorch/TF 导出 ONNX] end subgraph TFJS[Transformers.js] T1[Hugging Face 维护的浏览器推理库] T2[输入Hugging Face 上的模型自动转换] T3[后端ONNX Runtime Web作为依赖] T4[核心能力一键加载 HF 模型 Tokenizer Pipeline] T5[模型来源Hugging Face Hub 上的模型] end O3 -- Note[Transformers.js 底层也是\nONNX Runtime Web] T3 -.-|依赖关系| O3 style Note fill:#fff3e0 O1 --|角色定位| Role1[通用推理引擎\n适合自定义模型和性能优化] T1 --|角色定位| Role2[上层封装库\n适合快速实验和原型开发] style Role1 fill:#e3f2fd style Role2 fill:#e8f5e9关键发现Transformers.js 底层依赖 ONNX Runtime Web。所以选 Transformers.js 实际上是选了ONNX Runtime Web Hugging Face 生态的便利层。你用 Transformers.js 就是在间接用 ONNX Runtime Web。三、从开发体验开始对比3.1 Transformers.js 的使用体验Transformers.js 最大的优势是开箱即用。Hugging Face 上几千个模型命名规范、Tokenizer 配套、Pipeline 封装好了用一个 API 就能启动。// Transformers.js 使用示例 // 优势API 和 Python transformers 几乎一样学习成本极低 // 适合快速验证想法、原型开发、调用预训练模型 import { pipeline } from xenova/transformers; // 1. 情感分析 —— 一行代码搞定 // pipeline 会自动下载模型、Tokenizer 和配置文件 const classifier await pipeline( sentiment-analysis, // 任务类型 Xenova/distilbert-base-uncased-finetuned-sst-2-english // 模型名称 ); const result await classifier(This product is amazing!); // 输出[{ label: POSITIVE, score: 0.9998 }] // 2. 文本生成 const generator await pipeline( text-generation, Xenova/gpt2 // 注意GPT-2 级别的模型不是 GPT-3/4 ); const text await generator(Once upon a time, { max_new_tokens: 50, // 最多生成 50 个 token temperature: 0.8, // 生成多样性控制 do_sample: true, // 使用采样而非贪心解码 }); // 3. 词嵌入用于语义搜索 const embedder await pipeline( feature-extraction, Xenova/all-MiniLM-L6-v2 // 轻量级嵌入模型384 维 ); const embedding await embedder(什么是 WebAssembly, { pooling: mean, // 平均池化 normalize: true, // L2 归一化 }); // embedding.data: Float32Array(384)——可以用余弦相似度做搜索3.2 ONNX Runtime Web 的使用体验ONNX Runtime Web 更底层需要你自己管理模型文件、预处理输入、后处理输出。但换来的是完全的控制权。// ONNX Runtime Web 使用示例 // 优势完全的底层控制可以加载自定义 ONNX 模型 // 适合需要自定义预处理/后处理、对性能有极致要求 import * as ort from onnxruntime-web; // 配置推理后端 ort.env.wasm.wasmPaths /wasm/; // WASM 文件路径 // 1. 创建推理会话 // 这一步会下载 .onnx 文件并编译图 const session await ort.InferenceSession.create( /models/my-custom-model.onnx, // 你的 ONNX 模型文件 { executionProviders: [wasm], // 或 webgpu 如果浏览器支持 graphOptimizationLevel: all, // 启用全部图优化默认 } ); // 2. 预处理输入数据你需要自己实现 function preprocessText(text) { // 文本预处理示例 // 将文本转为模型需要的形式token IDs attention mask const tokens tokenize(text); // 自己实现的分词逻辑 const maxLength 128; // 输入张量需要以 BigInt64Array 的格式传递 // 因为 ONNX 模型要求整数输入 const inputIds new BigInt64Array(maxLength); const attentionMask new BigInt64Array(maxLength); // 填充 token IDs for (let i 0; i tokens.length; i) { inputIds[i] BigInt(tokens[i]); attentionMask[i] BigInt(1); // 实际 token 的位置设为 1 } // 剩余位置保持 0padding 的 mask 为 0 return { inputIds, attentionMask }; } // 3. 执行推理 async function runInference(text) { const { inputIds, attentionMask } preprocessText(text); // 创建 ONNX 张量 // dims: [batch_size, sequence_length] const feeds { input_ids: new ort.Tensor( int64, // ONNX 数据类型 inputIds, // 数据 [1, 128] // 形状1 个样本 x 128 tokens ), attention_mask: new ort.Tensor( int64, attentionMask, [1, 128] ), }; // 执行推理 const results await session.run(feeds); // 4. 后处理输出也需要自己实现 // results.logits: Tensor形状 [1, 128, vocab_size] const logits results.logits.data; // Float32Array return postprocess(logits); // 比如 softmax argmax 取预测类别 } // 5. WebGPU 后端配置性能更好但兼容性要求高 async function createWebGPUSession() { // 检测 WebGPU 是否可用 if (!navigator.gpu) { console.warn(WebGPU 不可用降级到 WASM 后端); return createWasmSession(); } return await ort.InferenceSession.create( /models/my-model.onnx, { executionProviders: [webgpu], // 使用 GPU 加速 // 注意不是所有算子都支持 WebGPU // 不支持的算子会自动回退到 WASM graphOptimizationLevel: all, } ); }对比下来直观的感受是Transformers.js写 5 行代码能跑起来但背后有 5 行你控不了的东西。ONNX Runtime Web写 50 行代码才能跑起来但这 50 行你都能控。四、性能和模型支持的对比4.1 加载时间在同一台机器M1 MacBook Pro上测了两个框架加载同一类模型DistilBERT的首屏加载时间gantt title 模型首次加载时间对比冷启动无缓存 dateFormat HH:mm:ss axisFormat %S section Transformers.js JS Bundle 下载 (500KB) :t1, 00:00:00, 1s ONNX 模型下载 (67MB) :t2, after t1, 2s Tokenizer JSON 解析 :t3, after t2, 1s WASM 运行时实例化 :t4, after t3, 2s 首次推理完成 :milestone, after t4, 0s section ONNX Runtime Web JS SDK 下载 (200KB) :o1, 00:00:00, 0.5s ONNX 模型下载 (67MB) :o2, after o1, 2s WASM 运行时实例化 :o3, after o2, 1.5s 首次推理完成 :milestone, after o3, 0sTransformers.js 多了 Tokenizer JSON 解析和 JS Bundle 的大小首次加载比直接用 ONNX Runtime Web 慢约 30%。但如果走 CDN 缓存第二次加载两边差距缩小到几乎可以忽略。4.2 推理速度对比 DistilBERT 单次推理的平均耗时M1 MacBookWASM 后端指标ONNX Runtime WebTransformers.js单次推理~35ms~38ms连续 100 次推理~3.2s~3.5s内存占用~120MB~150MB首屏加载~4.5s~6.0s两边的推理速度差距很小约 8%因为 Transformers.js 底层就是 ONNX Runtime Web。多出来的开销主要是 Tokenizer 的 JS 实现和 Pipeline 的封装层。4.3 模型支持范围这是我个人认为最关键的差异flowchart LR subgraph TFJS_Models[Transformers.js 支持的模型] T1[✅ Hugging Face 上千个模型] T2[✅ 文本BERT/GPT2/T5/DistilBERT...] T3[✅ 图像ViT/CLIP/ResNet...] T4[✅ 语音Whisper/Wav2Vec2...] T5[✅ 多模态CLIP/LayoutLM...] T6[⚠️ 大模型受限LLaMA 等太大无法在浏览器跑] T7[❌ 自定义 ONNX 模型不行] end subgraph ONNX_Models[ONNX Runtime Web 支持的模型] O1[✅ 任何 .onnx 文件] O2[✅ 跨框架PyTorch→ONNX, TF→ONNX, JAX→ONNX] O3[✅ 自定义架构你可以自己设计模型] O4[✅ 量化和剪枝后的模型] O5[⚠️ 算子限制部分算子不被 ONNX Runtime 支持] O6[❌ Tokenizer/预处理需要自己实现] end style T7 fill:#ffcdd2 style O6 fill:#ffcdd2 style T1 fill:#c8e6c9 style O1 fill:#c8e6c9根据上面的分析我给自己定了一个决策路径flowchart TD Q1{你的模型在 Hugging Face\n上已有配套实现} Q1 --|是| Q2{需要精细控制\n预处理/后处理逻辑} Q1 --|否自定义模型| Q3{你愿意自己实现\nTokenizer 和预处理吗} Q2 --|否默认就行| R1[ Transformers.js\n快速出活3 行代码搞定] Q2 --|是需要定制| R2[ ONNX Runtime Web\n完全控制但开发量大] Q3 --|是| R2 Q3 --|否太麻烦了| R3[⚠️ 考虑用 Transformers.js\n的相似模型代替] Q1 -- Q4{需要 WebGPU 加速} Q4 --|是| R2 Q4 --|否CPU 就行| R1 style R1 fill:#c8e6c9 style R2 fill:#e3f2fd style R3 fill:#fff3e0简化成一句话Hugging Face 上的标准模型用 Transformers.js自定义模型或需要极致性能优化用 ONNX Runtime Web。混合方案两种都用其实不用二选一。你可以在同一个项目里用两者// 混合方案Transformers.js 做文本预处理 ONNX Runtime 做推理 import { AutoTokenizer } from xenova/transformers; import * as ort from onnxruntime-web; async function mixedPipeline(text, onnxModelPath) { // 1. 用 Transformers.js 的 Tokenizer 做文本预处理 // 不需要自己实现分词逻辑 const tokenizer await AutoTokenizer.from_pretrained( Xenova/bert-base-uncased ); const encoded await tokenizer(text, { return_tensor: false, // 返回 JS 数组我们自己构建 ONNX Tensor }); // 2. 用 ONNX Runtime Web 执行自定义模型推理 const session await ort.InferenceSession.create(onnxModelPath); const feeds { input_ids: new ort.Tensor( int64, BigInt64Array.from(encoded.input_ids.map(BigInt)), [1, encoded.input_ids.length] ), attention_mask: new ort.Tensor( int64, BigInt64Array.from(encoded.attention_mask.map(BigInt)), [1, encoded.attention_mask.length] ), }; const outputs await session.run(feeds); // 3. 后处理 return outputs.logits.data; // 现在你有了 // - Transformers.js 提供的统一 Tokenizer 体验 // - ONNX Runtime Web 的自定义推理能力 // - 两边的好处都占了 }我在项目里就踩过这个坑混合模式下Transformers.js 的 Tokenizer 输出的 input_ids 是 Int32Array但 ONNX 模型要求 Int64类型不匹配导致推理结果全是 NaN。排查了一下午才发现要手动转 BigInt64Array 才能正常跑。五、总结在浏览器里跑模型推理这件事目前最好的两个选择就是它们俩。关键不是哪个更好而是你在乎什么快速出成果→ Transformers.js。Hugging Face 几千个模型一行 pipeline 就能跑。适合原型验证、快速实验。性能和控制的极致→ ONNX Runtime Web。全链路可控支持 WebGPU支持自定义 ONNX 模型。适合生产环境、对推理延迟敏感的实时场景。两者不互斥→ 大胆混用。Transformers.js 处理 TokenizerONNX Runtime 处理推理取长补短。注意浏览器限制内存一般不超过 4GB32 位浏览器 2GB模型太大如 LLaMA 7B跑不了。这些框架是在浏览器里跑小模型的最佳选择。WebGPU 是未来目前 WebGPU 的算子覆盖率还不全但推进很快。等覆盖率上来了浏览器端推理的速度会提升一个数量级。上面的分析来自我自己的实际使用体验可能有些观点不成熟欢迎在评论区指正和讨论。