Web Workers guide¶
The core API is worker-friendly because it does not require a DOM, a canvas, a
filesystem or a browser global. The worker receives an image-shaped value,
calls decode(), and sends plain result data back to the page. Move the
greyscale conversion and detector work off the UI thread when a camera or a
large file would otherwise make the page feel sticky. A JavaScript module worker
can import the package root or a public subpath; a TypeScript worker uses the
same package specifier after the application's bundler compiles it. Neither
should import implementation files from src/ts/.
A complete module-worker example¶
The following pair of files uses the public ESM bundle from a module worker. In a bundled application, the worker can import the npm package root instead.
barcode-worker.js:
import { decode } from
'https://unpkg.com/@sythos/js_barcode_universal/bundle/sythos-barcode.esm.js';
self.addEventListener('message', (event) => {
const { buffer, width, height, formats } = event.data;
const image = {
data: new Uint8ClampedArray(buffer),
width,
height,
};
try {
const results = decode(image, {
formats,
profile: 'camera',
tryHarder: true,
});
// DecodeResult values are structured-clone friendly. Send only the
// fields the UI needs if the application does not need full metadata.
self.postMessage({ ok: true, results });
} catch (error) {
self.postMessage({
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Worker barcode reader</title>
</head>
<body>
<video id="camera" playsinline muted autoplay></video>
<canvas id="frame" hidden></canvas>
<output id="status" role="status">Waiting for a frame</output>
<script type="module">
const video = document.querySelector('#camera');
const canvas = document.querySelector('#frame');
const status = document.querySelector('#status');
const worker = new Worker('./barcode-worker.js', { type: 'module' });
const context = canvas.getContext('2d', { willReadFrequently: true });
let busy = false;
worker.addEventListener('message', (event) => {
busy = false;
if (!event.data.ok) {
status.textContent = event.data.error;
return;
}
const result = event.data.results[0];
status.textContent = result
? `${result.format}: ${result.text}`
: 'No validated symbol in this frame';
});
async function start() {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false,
});
video.srcObject = stream;
await video.play();
function loop() {
requestAnimationFrame(loop);
if (busy || !video.videoWidth) return;
const scale = Math.min(1, 800 / video.videoWidth);
canvas.width = Math.max(1, Math.round(video.videoWidth * scale));
canvas.height = Math.max(1, Math.round(video.videoHeight * scale));
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const image = context.getImageData(0, 0, canvas.width, canvas.height);
busy = true;
// Transfer ownership of the pixel buffer so this frame is not copied.
worker.postMessage({
buffer: image.data.buffer,
width: image.width,
height: image.height,
formats: ['qr', 'datamatrix', 'pdf417'],
}, [image.data.buffer]);
}
loop();
}
start().catch((error) => {
status.textContent = `Camera unavailable: ${error.message || error}`;
});
</script>
</body>
</html>
The transfer list detaches the frame buffer from the page after
postMessage(). That is intentional: the page does not use that ImageData
again. If the page needs to retain the pixels, omit the transfer list and
accept the structured-clone copy, or create a separate buffer for the worker.
The example uses a camera only to demonstrate the worker boundary. Camera
permissions still require HTTPS or localhost, and iOS needs playsinline.
The camera guide covers the lifecycle and secure-context
failure cases.
Importing from an npm application¶
When a bundler owns the worker build, prefer a normal package import:
// barcode-worker.js
import { decode } from '@sythos/js_barcode_universal';
self.onmessage = ({ data }) => {
const image = {
data: new Uint8ClampedArray(data.buffer),
width: data.width,
height: data.height,
};
self.postMessage(decode(image, { formats: data.formats }));
};
Use a module worker ({ type: 'module' }) so the ESM import is preserved. A
classic worker cannot consume an ESM import without a bundler or a separate
script-loading strategy.
If the worker is authored in TypeScript, keep the request and response boundary typed while retaining the same runtime import:
import { decode, type DecodeResult } from '@sythos/js_barcode_universal';
type WorkerRequest = {
buffer: ArrayBuffer;
width: number;
height: number;
formats?: string[];
};
type WorkerResponse =
| { ok: true; results: DecodeResult[] }
| { ok: false; error: string };
self.addEventListener('message', (event: MessageEvent<WorkerRequest>) => {
const { buffer, width, height, formats } = event.data;
try {
const results = decode({
data: new Uint8ClampedArray(buffer),
width,
height,
}, { formats });
self.postMessage({ ok: true, results } satisfies WorkerResponse);
} catch (error) {
self.postMessage({
ok: false,
error: error instanceof Error ? error.message : String(error),
} satisfies WorkerResponse);
}
});
The worker still needs the application's normal TypeScript-to-JavaScript
build step; Node.js does not execute .ts files natively.
Message and result design¶
Keep the worker message boundary explicit. A practical message contains:
{
buffer: image.data.buffer,
width: image.width,
height: image.height,
formats: ['qr'],
}
The worker reconstructs Uint8ClampedArray and the SDK validates its dimensions
and byte length before detector work. Do not trust a width and height supplied
by an external page merely because the buffer was transferred; catch the
decoder error and report a safe status to the UI.
DecodeResult can include format, text, rotation, bounds,
confidence and format-specific metadata. Send the entire result when the UI
needs the camera overlay; otherwise project it to a small view model and keep
the raw payload out of an HTML sink. The security policy
defines the trust boundary for camera and file rasters.
Worker performance rules¶
- Keep one worker per sustained pipeline unless profiling proves otherwise.
- Use a
busyflag or a queue limit so a slow decode cannot build an unbounded backlog of camera frames. - Downscale before transfer when the symbol still has enough module detail.
- Restrict
formatsand avoidtryHarderretries when the capture contract is already controlled. - Transfer buffers when ownership can move; copy them when the main thread must continue using the same pixels.
- Stop the worker and camera stream when the page is hidden.
The performance guide explains why these choices matter and when the camera profile's eight orientation passes are worth the extra work.