Skip to content
FlashScan Pro

BarcodeDetector vs ZXing: decoding barcodes in the browser

The BarcodeDetector API decodes natively but isn't supported everywhere; a ZXing fallback covers the gap. Browser support, formats, and strategy.

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:

  1. Capability detection: if globalThis.BarcodeDetector exists, instantiate it with the useful formats (qr_code, ean_13, ean_8, code_128, data_matrix).
  2. Otherwise, dynamic import: await import('./zxingDecoder') loads ZXing on demand — Chromium users never pay the download cost.
  3. 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.
  4. Same pattern for images: detect() on an ImageBitmap when 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.