Two ways to decode in the browser
On the web today, reading a QR code or a barcode happens through one of two paths: the native BarcodeDetector API, built into the browser, and ZXing, the JavaScript port of the open-source decoding library. FlashScan Pro uses both — this article explains why, and how.
BarcodeDetector: the native path
BarcodeDetector is part of the Shape Detection API: the browser directly exposes its decoding capabilities, often implemented on top of system libraries. Typical usage:
const detector = new BarcodeDetector({ formats: ['qr_code', 'ean_13', 'code_128'] })
const codes = await detector.detect(videoElement) // or an ImageBitmap
Three decisive advantages: no dependency to load, platform-optimized decoding (often off the main thread), and a minimalist API — detect() takes a canvas, video, or bitmap and returns the codes found. The static BarcodeDetector.getSupportedFormats() method lists the formats actually supported by the current browser — essential, since the list varies across platforms.
The problem: browser support
The API ships in the Chromium family — Chrome, Edge, Opera, Chrome Android, and Android WebView. Firefox doesn’t implement it, and Safari doesn’t enable it by default. For a consumer-facing app, ignoring those browsers isn’t an option: you need a plan B.
ZXing: the universal decoder
ZXing is the JavaScript port of the decoding library that has powered much of the Android ecosystem for years. It works in pure software: pixels from the image or video are converted to luminance, binarized, then analyzed by a MultiFormatReader. In FlashScan Pro, the ZXing fallback is configured with 11 formats — QR Code, Data Matrix, Aztec, PDF417, Code 128, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF — plus the TRY_HARDER hint that forces a deeper analysis of difficult images.
The cost: a library to download and slower decoding. Which is why it should only load when needed.
The fallback strategy in practice
The app’s architecture illustrates the recommended pattern:
- Capability detection: if
globalThis.BarcodeDetectorexists, instantiate it with the useful formats (qr_code,ean_13,ean_8,code_128,data_matrix). - Otherwise, dynamic import:
await import('./zxingDecoder')loads ZXing on demand — Chromium users never pay the download cost. - Adapted cadence: camera scanning polls the native detector every 120 ms, and ZXing every 250 ms — the software fallback is more expensive, so it gets more time between frames.
- Same pattern for images:
detect()on anImageBitmapwhen the API exists, otherwise ZXing decodes the imported image’s pixels.
One caveat: a code read through the fallback may cover formats the native config doesn’t request (UPC-E, ITF, Aztec…). If your app targets specific formats, harmonize the lists — a Firefox user shouldn’t be able to scan a format rejected on Chrome.
Which should you choose?
- Internal app or controlled kiosk (Chrome guaranteed): BarcodeDetector alone is enough.
- Public-facing app: BarcodeDetector as the fast path, ZXing as a lazy fallback — that’s the performance/coverage trade-off powering this app’s scanner.
- Server-side or non-browser decoding: neither is built for that — ZXing Java or a native library will serve you better.