ONNX Runtime Web 导出功能端到端测试实践:基于 Next.js 与 Vite 的打包器兼容性验证
【免费下载链接】onnxruntimeONNX Runtime: cross-platform, high performance ML inferencing and training accelerator项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime
导读
onnxruntime-web 以纯 JavaScript/WebAssembly 方式在浏览器中运行 ONNX 模型,但其 ESM 打包产物能否被主流前端打包器正确解析、拆分与加载,直接影响开发者的集成体验。本文基于 ONNX Runtime 仓库中js/web/test/e2e/exports目录下的端到端测试工程,系统讲解如何用 Next.js(App Router 与 Turbopack)和 Vite(Vue + JavaScript)两类真实脚手架项目,配合 Puppeteer 自动化验证 onnxruntime-web 的导出功能。读完本文,你将掌握这两套测试用例的工程结构、多线程与 proxy 两种运行时配置的验证方法,以及如何在自己项目中复现同样的集成测试。
测试背景:为什么需要专门验证"导出功能"
onnxruntime-web 的打包产物包含.mjs格式的 ES Module 文件与.wasm二进制资源。不同打包器对 ESM 的静态分析、依赖预构建(pre-bundling)与资源内联策略各不相同,容易出现两类典型问题:
- 依赖预构建破坏 WebAssembly 加载:如 Vite 在
optimizeDeps阶段对使用 WebAssembly 的 npm 包进行预打包时可能出现加载异常(这是 Vite 5.x 时期的已知问题,测试工程中对此有专门处理,详见后文); - SSR/CSR 上下文不匹配:Next.js 默认在服务端渲染组件,而 onnxruntime-web 依赖浏览器环境(
window、WebAssembly、SharedArrayBuffer等),必须采用 CSR 组件或关闭 SSR 的动态导入方式。
因此,js/web/test/e2e/exports/README.md 专门建立一个独立的测试目录,用两个真实脚手架应用(nextjs-default 与 vite-default)来覆盖最常见的 React 与 Vue 生态打包链路。
测试工程总览
整个导出测试位于仓库的 js/web/test/e2e/exports 目录,结构如下:
js/web/test/e2e/exports/ ├── README.md # 测试说明与用例导航 ├── main.js # 测试入口:安装依赖、调度 dev/prod 测试 ├── test.js # 测试执行:启动服务器 + Puppeteer 浏览器自动化 ├── utils.js # 通用工具:shell 命令执行、依赖安装、进程树清理 └── testcases/ ├── nextjs-default.md # Next.js 用例说明 ├── vite-default.md # Vite 用例说明 ├── nextjs-default/ # Next.js 脚手架应用 └── vite-default/ # Vite 脚手架应用三个核心脚本各司其职,形成一条清晰的测试流水线:
- utils.js 中的
installOrtPackages()负责在每个测试用例目录下安装依赖(默认执行npm ci,若传入额外包则执行npm install),runShellCmd()以子进程方式运行命令并通过事件机制感知服务器"就绪"; - main.js 作为入口,按 nextjs-default → vite-default 的顺序依次调度各用例的 dev 测试、Turbopack 测试、生产构建测试及产物校验;
- test.js 中的
launchBrowserAndRunTests()使用puppeteer-core驱动本机 Chrome,在页面上模拟用户点击并校验推理结果。
nextjs-default:Next.js 下的 onnxruntime-web 集成验证
脚手架创建方式
根据 nextjs-default.md 的记录,该用例由npx create-next-app@latest交互式生成,创建参数如下(全部保持默认值,除项目名外均选 No):
√ What is your project named? ... nextjs-default √ Would you like to use TypeScript? ... No √ Would you like to use ESLint? ... No √ Would you like to use Tailwind CSS? ... No √ Would you like your code inside a `src/` directory? ... No √ Would you like to use App Router? (recommended) ... No √ Would you like to use Turbopack for `next dev`? ... No √ Would you like to customize the import alias (`@/*` by default)? ... No从 package.json 可以看到实际依赖为next@^15.5.24、react@^19.0.0、react-dom@^19.0.0,并保留了默认的dev/build/start三个脚本,同时通过overrides固定了postcss版本,保证脚手架在后续npm ci时可复现安装。虽然向导中选择不使用 App Router,但当前代码实际上落在app/目录结构下(app/layout.js与app/page.js),这是测试随仓库演进的结果。
基于模板的针对性改造
在脚手架基础上,测试工程做了两类改造(两份用例文档描述一致):
- 清理:删除默认的 Logo、图片、CSS 与 SVG,避免无关资源干扰测试;
- 新增测试组件:添加一个纯客户端渲染(CSR)组件,内含:
- 一个多线程(Multi-thread)复选框
#cb-mt; - 一个代理(Proxy)复选框
#cb-px; - 一个 "Load Model" 按钮
#btn-load; - 一个 "Run Model" 按钮
#btn-run; - 一个状态显示 DIV
#ortstate与一个日志显示 DIV#ortlog;
- 一个多线程(Multi-thread)复选框
- 新增 helper 模块:负责创建 ORT Session 并执行推理验证。
改造后的 CSR 组件实现在 onnx-test-bar.js 中,其关键点在于:
'use client'; import { useState } from 'react'; export default function OnnxTestBar() { const [ortState, setOrtState] = useState(0); const [ortLog, setOrtLog] = useState('Ready.'); ... const loadModel = async () => { setOrtState(1); ... const { createTestSession } = await import('./onnx-helper'); await createTestSession(document.getElementById('cb-mt').checked, document.getElementById('cb-px').checked); ... };组件用useState维护一个从 0 到 6 的状态机,loadModel与runTest均通过动态import()按需加载 onnx-helper 模块——这既模拟了真实业务中按需加载 onnxruntime-web 的场景,也验证了打包器对动态导入的正确切分。状态机的含义为:
| 状态值 | 含义 |
|---|---|
| 0 | 初始就绪(两个按钮可用) |
| 1 | 正在加载模型 |
| 2 | 模型加载成功(激活 "Run Test" 按钮) |
| 3 | 模型加载失败(日志区显示错误) |
| 4 | 正在运行推理测试 |
| 5 | 推理测试通过 |
| 6 | 推理测试失败 |
在 page.js 中,该组件通过next/dynamic并以ssr: false挂载,这是 Next.js 中规避服务端渲染执行浏览器专属代码的标准做法:
'use client'; import dynamic from 'next/dynamic'; const OnnxTestBarComponent = dynamic(() => import('../components/onnx-test-bar'), { ssr: false }); export default function Home() { return ( <div> <main> <OnnxTestBarComponent /> </main> </div> ); }同时 next.config.mjs 保持默认空配置,意味着这套测试覆盖的是 Next.js无自定义 webpack 配置时的开箱即用导出行为。
onnx-helper:会话创建与推理验证
onnx-helper.js 是验证逻辑的核心,它演示了 onnxruntime-web 的两个关键运行时开关:
import * as ort from 'onnxruntime-web'; export const createTestSession = async (multiThreaded, proxy) => { const model = base64StringToUint8Array(testModelData); const options = {}; if (multiThreaded) { ort.env.wasm.numThreads = 2; assert(typeof SharedArrayBuffer !== 'undefined', 'SharedArrayBuffer is not supported'); } if (proxy) { ort.env.wasm.proxy = true; } mySession = await ort.InferenceSession.create(model, options); };- 多线程(
ort.env.wasm.numThreads = 2):启用 WASM 多线程需要页面具备SharedArrayBuffer能力,即必须通过 COOP/COEP 响应头开启跨源隔离(cross-origin isolation)。代码在启用前用assert显式检查SharedArrayBuffer是否存在,这正是浏览器端多线程最容易被忽视的前置条件; - 代理(
ort.env.wasm.proxy = true):将 onnxruntime-web 的 WASM 计算放入独立 Web Worker 中执行,避免阻塞主线程,这是生产环境提升 UI 流畅度的常见配置。
模型数据采用内联 base64 字符串解码为Uint8Array(测试模型为test_abs/model.onnx,对应 Abs 算子),因此无需网络请求即可加载,测试更稳定。推理验证部分构造 60 个正负交替的浮点数(形状[3, 4, 5]),运行模型后逐元素断言输出等于输入的绝对值:
const inputData = [...Array(60).keys()].map((i) => (i % 2 === 0 ? i : -i)); const expectedOutputData = inputData.map((i) => Math.abs(i)); const fetches = await mySession.run({ x: new ort.Tensor('float32', inputData, [3, 4, 5]) }); const y = fetches.y; assert(y instanceof ort.Tensor, 'unexpected result'); assert(y.dims.length === 3 && y.dims[0] === 3 && y.dims[1] === 4 && y.dims[2] === 5, 'incorrect shape'); for (let i = 0; i < expectedOutputData.length; i++) { assert(y.data[i] === expectedOutputData[i], `output data mismatch at index ${i}`); } return 'PASS';这里同时校验了输出张量的类型、维度([3, 4, 5])与 60 个数值的逐项正确性,属于端到端级别的结果验证。
测试矩阵
根据文档,nextjs-default 用例在三种服务器模式下分别跑 2×2 全组合:
| 服务器模式 | 组合 | 说明 |
|---|---|---|
npm run dev | 多线程 OFF/ON × 代理 OFF/ON | 开发服务器 |
npm run dev -- --turbopack | 多线程 OFF/ON × 代理 OFF/ON | 开发服务器(Turbopack) |
npm run build+npm run start | 多线程 OFF/ON × 代理 OFF/ON | 生产模式 |
其中npm run dev -- --turbopack专门覆盖 Next.js 的 Rust 版打包器 Turbopack,验证其在开发模式下对 onnxruntime-web 的处理;生产模式则验证next build产物(含资源指纹与代码分割)在next start下可正常运行。
vite-default:Vite + Vue 下的集成验证
脚手架创建方式
vite-default.md 记录该用例由npm create vite@latest生成,选择了 Vue 框架与 JavaScript 变体:
√ Project name: ... vite-default √ Select a framework: » Vue √ Select a variant: » JavaScript生成后按提示cd vite-default && npm install && npm run dev即可启动。
模板改造与 Vite 关键配置
改造内容与 nextjs-default 一致:删除默认 Logo/图片/CSS/SVG,新增包含两个复选框、两个按钮、状态 DIV 与日志 DIV 的 CSR 组件,并添加同构的 onnx-helper 模块(onnx-helper.js 与 Next.js 版逻辑完全相同,会话创建、多线程/proxy 开关、Abs 模型推理验证均一致)。页面侧的逻辑落在 Vue 的 HelloWorld.vue 中,同样通过动态import('./onnx-helper')按需加载。
Vite 版最重要的差异化配置在 vite.config.js:
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ // This is a known issue when using WebAssembly with Vite 5.x // Need to specify `optimizeDeps.exclude` to NPM packages that uses WebAssembly // See: https://github.com/vitejs/vite/issues/8427 optimizeDeps: { exclude: ['onnxruntime-web'], }, plugins: [vue()], });optimizeDeps.exclude: ['onnxruntime-web']是这套用例验证的重点:Vite 默认会对依赖进行预构建(esbuild 预打包),而使用 WebAssembly 的包在预构建阶段存在已知兼容问题,必须将其排除在预构建之外,让 onnxruntime-web 以原生 ESM 方式被浏览器直接加载。该配置项也因此成为"onnxruntime-web + Vite"集成的必备配置。
测试矩阵与产物校验
vite-default 的测试矩阵略少于 Next.js(无 Turbopack 变体):
| 模式 | 组合 | 端口 |
|---|---|---|
npm run dev | 多线程 OFF/ON × 代理 OFF/ON | 5173 |
npm run build+npm run start | 多线程 OFF/ON × 代理 OFF/ON | 4173 |
除此之外,main.js 在 vite-default 的生产构建完成后还执行了一次产物校验(verifyAssets):
await verifyAssets('vite-default', async (cwd) => { const globby = await import('globby'); return { test: 'File "dist/assets/**/ort.*.mjs" should not exist', success: globby.globbySync('dist/assets/**/ort.*.mjs', { cwd }).length === 0, }; });该断言检查生产构建产物dist/assets/下不应残留独立的ort.*.mjs文件——如果 onnxruntime-web 的 ESM 产物未被正确合并进 chunk,就会在dist/assets中出现独立的ort.*.mjs资源,说明打包器对 onnxruntime-web 的导出处理异常。这是对"导出功能"最直接的产物级验证,也是本测试目录命名的由来。
端到端执行原理:Puppeteer 驱动浏览器模拟
test.js 中的launchBrowserAndRunTests()负责整个浏览器自动化流程,其完整链路为:
- 用
puppeteer-core启动本机 Chrome(headless: true),并附加--enable-features=SharedArrayBuffer启动参数(保证多线程用例可用,与 onnx-helper 中的 SharedArrayBuffer 检查呼应); - 打开
http://localhost:<port>,等待#ortstate可见; - 根据组合勾选
#cb-mt(多线程)与#cb-px(代理)复选框; - 点击
#btn-load,用waitForFunction等待ortstate进入'2'(成功)或'3'(失败),若失败则从#ortlog读取错误日志; - 点击
#btn-run,等待ortstate进入'5'(通过)或'6'(失败),同样读取#ortlog定位问题; - 汇总四条组合的结果,任一失败即整体判定该模式测试失败。
服务器侧的编排在runTest()中:先用runShellCmd以子进程方式启动npm run dev或npm run start,通过监听 stdout 中特定"就绪"字符串来感知服务器可用——Next.js 的就绪标志是✓ Ready in,Vite 的就绪标志是终端带颜色的➜ Local:(代码中以转义序列\x1b[32m➜\x1b[39m匹配);收到就绪事件后立即执行浏览器测试,结束后通过tree-kill递归杀掉整个进程树,防止端口残留。生产模式(runProdTest)则先执行npm run build,再对next start/vite preview启动的服务器跑同一套浏览器用例。
本地复现与扩展指南
在具备 Node.js 与 Chrome 环境的前提下,可按以下步骤复现这套导出测试:
- 确认测试入口:整体入口为 main.js 的
main(PRESERVE, PACKAGES_TO_INSTALL)函数,PRESERVE为真时跳过依赖安装,PACKAGES_TO_INSTALL用于指定额外安装的包(如指向本地构建的 onnxruntime-web tarball); - 安装依赖:首次运行会依次在
testcases/nextjs-default与testcases/vite-default下执行npm ci(或按传入参数执行npm install <pkg>);由于 onnxruntime-web 依赖外置 npm 包,建议结合构建脚本将仓库内最新构建的包注入PACKAGES_TO_INSTALL,以验证本地改动; - 运行测试:脚本会依次执行——nextjs-default 的 dev 模式(含 Turbopack)与生产模式、vite-default 的 dev 与生产模式,以及 vite-default 的
dist产物校验;每轮四组合全部通过才算成功; - 按需扩展:如需覆盖更多打包器或框架(如 Svelte、Astro、Webpack 5 手动配置),可参照现有两个用例的模式——用官方脚手架生成项目 → 添加相同的 CSR 测试组件与 onnx-helper 模块 → 在 main.js 中注册
runDevTest/runProdTest(必要时补充verifyAssets产物断言),即可复用同一套 Puppeteer 自动化框架。
小结
onnxruntime-web 的导出功能测试并不是简单的"能否 import",而是通过两个贴近真实工程的最小脚手架,覆盖了三大类集成风险:WebAssembly 依赖的预构建冲突(ViteoptimizeDeps.exclude)、SSR/CSR 环境隔离(Next.jsdynamic+ssr: false)、多线程与代理等运行时开关(SharedArrayBuffer、numThreads、proxy)。配合 Puppeteer 的 2×2 组合矩阵与产物级断言,为 onnxruntime-web 的每个发布版本提供了可靠的打包器兼容性保障,也为使用 React/Vue 生态的开发者提供了可直接借鉴的集成样板。
【免费下载链接】onnxruntimeONNX Runtime: cross-platform, high performance ML inferencing and training accelerator项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考