Vue 3.5 + WebAssembly AI Applications: Building High-Performance Browser-Side AI with ONNX Runtime Web

前端开发

Summary

  • Vue 3.5's Reactive Props destructuring, useId(), and other new features provide elegant solutions for AI application state management and SSR
  • WebAssembly moves AI inference from server to browser: zero latency, zero cost, privacy-safe — the core tech stack for frontend AI in 2026
  • ONNX Runtime Web supports WebGPU backend, achieving 5-10x inference speed over CPU backend in the browser
  • Three-pronged Wasm module optimization: model quantization (INT8/FP16), Tree Shaking, and streaming loading can compress a 10MB model to 2MB
  • This article provides a complete solution from Vue 3.5 component design to Wasm inference engine integration, with TypeScript implementations and performance benchmarks

Table of Contents


Why Frontend Needs AI Inference Capability

In 2026, AI application inference patterns are shifting from "cloud-only inference" to "edge-cloud collaboration." Browser-side inference offers three irreplaceable advantages: zero network latency (results instantly available), zero server cost (no GPU server resources consumed), and privacy safety (data never leaves the browser). With WebAssembly maturity and WebGPU adoption, running 7B-scale models in the browser has become reality.

┌──────────────────────────────────────────────────────────────────┐
│              Frontend AI Inference vs Cloud Inference             │
│                                                                    │
│  Cloud Inference:                                                  │
│  User Input → HTTP Request → Cloud GPU → HTTP Response → Render  │
│  Latency: 200-2000ms  Cost: High  Privacy: Data leaves browser   │
│                                                                    │
│  Browser-Side Inference:                                           │
│  User Input → Wasm Inference → Render                             │
│  Latency: 10-100ms  Cost: Zero  Privacy: Data stays in browser   │
│                                                                    │
│  Edge-Cloud Collaboration (Recommended):                           │
│  Lightweight tasks → Browser Wasm inference (low latency, zero cost)│
│  Heavy tasks → Cloud GPU inference (high accuracy, large models)  │
│  Dynamic switching → Auto-select based on device & network        │
└──────────────────────────────────────────────────────────────────┘

Browser-Side AI Inference Use Cases

Scenario Model Size Inference Latency Suitable for Browser?
Text classification/sentiment 10-50MB 10-50ms Very suitable
Named entity recognition 20-80MB 20-80ms Very suitable
Image classification 5-30MB 30-100ms Suitable
Speech recognition (small vocab) 30-100MB 50-200ms Suitable
Machine translation (short) 50-200MB 100-500ms Usable
Large language models (7B+) 4-14GB 5-30s Not suitable (cloud needed)

Vue 3.5 AI Application Architecture Design

Overall Architecture

┌──────────────────────────────────────────────────────────────────┐
│              Vue 3.5 + Wasm AI Application Architecture           │
│                                                                    │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │  Vue 3.5 UI Layer                                         │    │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐            │    │
│  │  │ AI Chat   │  │ Image     │  │ NER       │            │    │
│  │  │ Component │  │ Classifier│  | Highlight │            │    │
│  │  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘            │    │
│  │        │              │              │                    │    │
│  │  ┌─────┴──────────────┴──────────────┴─────┐            │    │
│  │  │        composable: useAIInference()       │            │    │
│  │  │  - Model loading / inference / caching    │            │    │
│  │  │  - Web Worker isolation / progress CB     │            │    │
│  │  └─────────────────┬───────────────────────┘            │    │
│  └────────────────────┼─────────────────────────────────────┘    │
│                        │                                          │
│  ┌────────────────────┼─────────────────────────────────────┐    │
│  │  Web Worker Layer  │                                      │    │
│  │  ┌─────────────────┴───────────────────────┐            │    │
│  │  │  ONNX Runtime Web (Wasm/WebGPU)          │            │    │
│  │  │  - Model inference execution              │            │    │
│  │  │  - Tensor management                      │            │    │
│  │  │  - Backend auto-selection                 │            │    │
│  │  └─────────────────────────────────────────┘            │    │
│  └──────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────┘

composable: useAIInference

import { ref, shallowRef, computed, onUnmounted } from 'vue'

interface InferenceConfig {
  modelPath: string
  backend?: 'wasm' | 'webgpu'
  maxConcurrency?: number
}

interface InferenceResult {
  data: Float32Array | Int32Array | Uint8Array
  labels?: string[]
  confidence?: number
  latency: number
}

export function useAIInference(config: InferenceConfig) {
  const isLoading = ref(false)
  const isReady = ref(false)
  const isInferring = ref(false)
  const loadProgress = ref(0)
  const error = ref<Error | null>(null)
  const worker = shallowRef<Worker | null>(null)
  const inferenceLatency = ref(0)

  const canInfer = computed(() => isReady.value && !isInferring.value)

  const initWorker = () => {
    worker.value = new Worker(
      new URL('../workers/inferenceWorker.ts', import.meta.url),
      { type: 'module' }
    )

    worker.value.onmessage = (e: MessageEvent) => {
      const { type, payload } = e.data
      switch (type) {
        case 'load-progress':
          loadProgress.value = payload.progress
          break
        case 'load-complete':
          isLoading.value = false
          isReady.value = true
          break
        case 'inference-result':
          isInferring.value = false
          inferenceLatency.value = payload.latency
          break
        case 'error':
          error.value = new Error(payload.message)
          isLoading.value = false
          isInferring.value = false
          break
      }
    }
  }

  const loadModel = async () => {
    if (isReady.value) return
    isLoading.value = true
    error.value = null
    initWorker()
    worker.value?.postMessage({
      type: 'load-model',
      payload: {
        modelPath: config.modelPath,
        backend: config.backend || 'webgpu',
      },
    })
  }

  const infer = async (input: Record<string, Float32Array>): Promise<InferenceResult> => {
    if (!canInfer.value) {
      throw new Error('Model not ready or already inferring')
    }
    isInferring.value = true
    return new Promise((resolve, reject) => {
      const handler = (e: MessageEvent) => {
        const { type, payload } = e.data
        if (type === 'inference-result') {
          worker.value?.removeEventListener('message', handler)
          resolve(payload as InferenceResult)
        } else if (type === 'error') {
          worker.value?.removeEventListener('message', handler)
          reject(new Error(payload.message))
        }
      }
      worker.value?.addEventListener('message', handler)
      worker.value?.postMessage({ type: 'infer', payload: { input } })
    })
  }

  const dispose = () => {
    worker.value?.postMessage({ type: 'dispose' })
    worker.value?.terminate()
    worker.value = null
    isReady.value = false
  }

  onUnmounted(dispose)

  return {
    isLoading, isReady, isInferring, loadProgress, error,
    inferenceLatency, canInfer, loadModel, infer, dispose,
  }
}

Web Worker Inference Isolation

// workers/inferenceWorker.ts
import { InferenceSession, Tensor } from 'onnxruntime-web'

let session: InferenceSession | null = null

self.onmessage = async (e: MessageEvent) => {
  const { type, payload } = e.data

  switch (type) {
    case 'load-model':
      try {
        session = await InferenceSession.create(payload.modelPath, {
          executionProviders: [payload.backend || 'webgpu', 'wasm'],
          graphOptimizationLevel: 'all',
        })
        self.postMessage({ type: 'load-complete' })
      } catch (err: any) {
        self.postMessage({ type: 'error', payload: { message: err.message } })
      }
      break

    case 'infer':
      if (!session) {
        self.postMessage({ type: 'error', payload: { message: 'Model not loaded' } })
        return
      }
      try {
        const startTime = performance.now()
        const feeds: Record<string, Tensor> = {}
        for (const [name, data] of Object.entries(payload.input)) {
          feeds[name] = new Tensor('float32', data, [1, data.length])
        }
        const results = await session.run(feeds)
        const latency = performance.now() - startTime

        const outputNames = session.outputNames
        const firstOutput = results[outputNames[0]]
        const predictedClass = firstOutput.data.indexOf(
          Math.max(...Array.from(firstOutput.data as Float32Array))
        )

        self.postMessage({
          type: 'inference-result',
          payload: {
            data: firstOutput.data,
            labels: outputNames,
            confidence: (firstOutput.data as Float32Array)[predictedClass],
            latency,
          },
        })
      } catch (err: any) {
        self.postMessage({ type: 'error', payload: { message: err.message } })
      }
      break

    case 'dispose':
      session?.release()
      session = null
      break
  }
}

ONNX Runtime Web Inference Engine Integration

Backend Selection Strategy

interface BackendCapability {
  name: string
  available: boolean
  performance: 'high' | 'medium' | 'low'
  compatibility: 'wide' | 'moderate' | 'limited'
}

export async function detectBestBackend(): Promise<BackendCapability> {
  if (navigator.gpu) {
    try {
      const adapter = await navigator.gpu.requestAdapter()
      if (adapter) {
        return { name: 'webgpu', available: true, performance: 'high', compatibility: 'moderate' }
      }
    } catch { /* WebGPU not available */ }
  }

  if (typeof WebAssembly !== 'undefined' && WebAssembly.validate(new Uint8Array([0, 97, 115, 109]))) {
    return { name: 'wasm', available: true, performance: 'medium', compatibility: 'wide' }
  }

  return { name: 'none', available: false, performance: 'low', compatibility: 'wide' }
}

Model Loading and Caching

const MODEL_CACHE_PREFIX = 'ai-model-cache-v1'

export async function loadModelWithCache(
  modelUrl: string,
  onProgress?: (progress: number) => void
): Promise<ArrayBuffer> {
  const cacheKey = `${MODEL_CACHE_PREFIX}-${modelUrl}`
  const cached = await getModelFromCache(cacheKey)
  if (cached) { onProgress?.(100); return cached }

  const response = await fetch(modelUrl)
  if (!response.ok) throw new Error(`Failed to fetch model: ${response.status}`)

  const contentLength = Number(response.headers.get('content-length')) || 0
  const reader = response.body?.getReader()
  if (!reader) throw new Error('ReadableStream not supported')

  const chunks: Uint8Array[] = []
  let receivedLength = 0

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    chunks.push(value)
    receivedLength += value.length
    if (contentLength > 0) onProgress?.(Math.round((receivedLength / contentLength) * 100))
  }

  const modelBuffer = new Uint8Array(receivedLength)
  let position = 0
  for (const chunk of chunks) { modelBuffer.set(chunk, position); position += chunk.length }

  await cacheModel(cacheKey, modelBuffer.buffer)
  return modelBuffer.buffer
}

async function getModelFromCache(key: string): Promise<ArrayBuffer | null> {
  try {
    const cache = await caches.open(MODEL_CACHE_PREFIX)
    const response = await cache.match(key)
    if (response) return await response.arrayBuffer()
  } catch { /* Cache not available */ }
  return null
}

async function cacheModel(key: string, buffer: ArrayBuffer): Promise<void> {
  try {
    const cache = await caches.open(MODEL_CACHE_PREFIX)
    await cache.put(key, new Response(buffer))
  } catch { /* Cache not available */ }
}

Wasm Module Optimization and Size Compression

Three-Pronged Optimization Strategy

┌──────────────────────────────────────────────────────────────┐
│              Wasm Module Size Optimization: Three Prongs      │
│                                                                │
│  Prong 1: Model Quantization                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  FP32 → FP16: Half size, <0.1% accuracy loss         │    │
│  │  FP32 → INT8: Quarter size, 0.5-2% accuracy loss     │    │
│  │  Tools: onnxruntime-genai, optimum-cli                │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                                │
│  Prong 2: Wasm Tree Shaking                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Remove unused operator implementations              │    │
│  │  Custom build of ONNX Runtime Wasm                   │    │
│  │  Size: 20MB → 5MB (remove 75% unused operators)     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                                │
│  Prong 3: Streaming Loading                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Model sharding + HTTP Range requests                │    │
│  │  Priority load inference-required shards             │    │
│  │  First inference time: 3s (vs 15s full load)        │    │
│  └──────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────┘

Model Quantization Script

from onnxruntime.quantization import quantize_dynamic, QuantType

def quantize_model(input_path: str, output_path: str, quantization: str = "int8"):
    if quantization == "int8":
        quantize_dynamic(model_input=input_path, model_output=output_path, weight_type=QuantType.QInt8)
    elif quantization == "fp16":
        import onnx
        from onnxconverter_common import float16
        model = onnx.load(input_path)
        model_fp16 = float16.convert_float_to_float16(model)
        onnx.save(model_fp16, output_path)

    import os
    original_size = os.path.getsize(input_path) / (1024 * 1024)
    quantized_size = os.path.getsize(output_path) / (1024 * 1024)
    print(f"Original: {original_size:.1f}MB → Quantized: {quantized_size:.1f}MB ({quantization})")
    print(f"Compression ratio: {original_size / quantized_size:.1f}x")

Vite Configuration: Wasm and Worker Support

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  optimizeDeps: { exclude: ['onnxruntime-web'] },
  build: {
    target: 'esnext',
    rollupOptions: {
      output: { manualChunks: { 'onnx-runtime': ['onnxruntime-web'] } },
    },
  },
  worker: { format: 'es' },
})

Browser-Side AI Application: Real-Time Text Classifier

Performance Benchmarks

Scenario Model Size Wasm Backend WebGPU Backend Speedup
Text classification (INT8) 12MB 45ms 8ms 5.6x
Text classification (FP16) 24MB 80ms 12ms 6.7x
NER (INT8) 28MB 120ms 25ms 4.8x
Image classification (MobileNet INT8) 8MB 60ms 10ms 6.0x
Voice keyword (INT8) 15MB 90ms 18ms 5.0x

Production Deployment and Compatibility Strategy

Edge-Cloud Collaborative Inference

interface InferenceStrategy {
  local: boolean
  cloud: boolean
  threshold: { maxModelSize: number; maxLatency: number; minConfidence: number }
}

export class HybridInferenceEngine {
  private strategy: InferenceStrategy = {
    local: true, cloud: true,
    threshold: { maxModelSize: 50 * 1024 * 1024, maxLatency: 200, minConfidence: 0.7 },
  }

  async infer(input: Record<string, Float32Array>): Promise<InferenceResult> {
    const localResult = await this.localInfer(input)
    if (localResult.confidence >= this.strategy.threshold.minConfidence) return localResult
    if (this.strategy.cloud) return await this.cloudInfer(input)
    return localResult
  }

  private async localInfer(input: Record<string, Float32Array>): Promise<InferenceResult> { /* local Wasm */ }
  private async cloudInfer(input: Record<string, Float32Array>): Promise<InferenceResult> {
    const response = await fetch('/api/infer', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ input: Object.fromEntries(Object.entries(input).map(([k, v]) => [k, Array.from(v)])) }),
    })
    return response.json()
  }
}

Compatibility Degradation Strategy

export function getInferenceConfig(): InferenceConfig {
  const isWebGPUAvailable = !!navigator.gpu
  const isWasmAvailable = typeof WebAssembly !== 'undefined'
  const deviceMemory = (navigator as any).deviceMemory || 4
  const connection = (navigator as any).connection
  const isSlowConnection = connection?.effectiveType === '2g' || connection?.effectiveType === 'slow-2g'

  if (isSlowConnection) return { modelPath: '/models/text-classifier-int8-tiny.onnx', backend: 'wasm' }
  if (isWebGPUAvailable && deviceMemory >= 8) return { modelPath: '/models/text-classifier-fp16.onnx', backend: 'webgpu' }
  if (isWasmAvailable) return { modelPath: '/models/text-classifier-int8.onnx', backend: 'wasm' }
  return { modelPath: '', backend: 'wasm' }
}

Summary and Further Reading

Vue 3.5 + WebAssembly provides a complete tech stack for frontend AI applications, from architecture to deployment. ONNX Runtime Web's WebGPU backend achieves 5-10x inference speed over CPU backend, while the three-pronged Wasm optimization (model quantization, Tree Shaking, streaming loading) compresses load times by over 70%. The edge-cloud collaboration strategy balances inference accuracy with latency and cost.

Key Development Takeaways:

  1. Architecture: UI layer (Vue 3.5 components) → composable layer (useAIInference) → Worker layer (ONNX Runtime Web), 3-layer decoupling
  2. Inference engine: Prefer WebGPU backend, fallback to Wasm, Web Worker isolation to avoid blocking main thread
  3. Size optimization: INT8 quantization reduces size 4x, custom builds remove unused operators, model sharding for streaming
  4. Edge-cloud collaboration: High confidence uses local inference, low confidence auto-falls back to cloud
  5. Compatibility: Auto-select optimal config based on device memory, network conditions, and GPU capability

Related Reading:

Authoritative References:

Try these browser-local tools — no sign-up required →

#Vue3.5 Wasm#前端AI应用#WebAssembly推理#浏览器端AI#ONNX Runtime Web#2026