Concepts

Execution providers

WebGPU or WebAssembly, how many threads, and why cross-origin isolation decides the answer.

configure({ executionProviders: ['webgpu', 'wasm'] })

Order is priority: WebGPU where available, WASM otherwise. This is passed straight to onnxruntime-web when a session is created.

Prefer WebGPU

It is typically 2–5× faster and needs no cross-origin isolation, which makes it the simpler answer wherever it exists:

import { isWebGpuAvailable } from '@stabrise/scaledp/ocr'

configure({
    executionProviders: (await isWebGpuAvailable()) ? ['webgpu', 'wasm'] : ['wasm'],
})

isWebGpuAvailable() requests an adapter rather than checking for navigator.gpu, because the object exists in browsers where no adapter can actually be acquired.

Two caveats. GlinerNer with the GLiNER2 model overrides this and always uses WASM — see the warning. And onnxruntime-web deprecated the WebGL and JSEP providers in 1.29; the native WebGPU provider is the supported path.

Threads

configure({ numThreads: 4 })   // 0 (default) derives from hardwareConcurrency

The default is max(1, min(4, hardwareConcurrency - 1)) — one core left for the UI, capped at four, because past that ORT's own synchronisation overhead outweighs the gain at these model sizes. Four cores are assumed when the browser will not say.

import { defaultNumThreads, resolveNumThreads } from '@stabrise/scaledp'

The value is baked into a session when it is created, so changing it later requires a fresh stage.

Threads need cross-origin isolation

Multi-threaded WebAssembly needs SharedArrayBuffer, which needs the page to be cross-origin isolated:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Without them, crossOriginIsolated is false, ORT falls back to its single-threaded build, and numThreads has no effect at all.

import { isCrossOriginIsolated } from '@stabrise/scaledp/ocr'

if (!isCrossOriginIsolated()) {
    console.info('Single-threaded WASM: set COOP/COEP headers, or prefer WebGPU.')
}

This is a property of the page, not the worker — moving work into a worker does not escape it. A library cannot set headers for its consumer, so the situation is reported rather than enforced.

require-corp also blocks every cross-origin subresource that is not CORS-fetched: fonts, images, embeds, analytics. Turning it on is a decision about the whole page, which is the other reason WebGPU is usually the easier path.

Deciding

WebGPUWASM, threadedWASM, single
Speedfastestmiddleslowest
Needs COOP/COEPnoyesno
Availabilityrecent browsers, real GPUany, with headerseverywhere
GLiNER2unusable — drops entitiesfinefine

The demo's own configuration is the general-purpose answer:

const webgpu = await isWebGpuAvailable()
configure({ executionProviders: webgpu ? ['webgpu', 'wasm'] : ['wasm'] })

On this page