ZXing WASM vs. Dynamsoft Barcode Reader: A JavaScript Barcode Benchmark on 7,894 Real Images
This benchmark scores two browser barcode readers — the open-source zxing-wasm package (3.1.2, the ZXing-C++ core compiled to WebAssembly) and Dynamsoft Barcode Reader (dynamsoft-barcode-reader-bundle 11.4.3000) — on 7,894 deduplicated images drawn from BarBeR, a public real-world barcode dataset. Every decode result ships with the project so any reviewer can re-check the numbers, and because the run reuses the exact audited manifest from the C++ and Python articles in this series, the three reads as one controlled experiment: identical input, identical scoring rules, a different language/runtime binding.

What you’ll build: A browser-driven JavaScript benchmark that streams BarBeR images through a Node collector, times zxing-wasm and Dynamsoft’s CaptureVisionRouter on identical pixels, validates the result records, and ships the raw JSONL stream, a combined JSON package, and a self-contained HTML report.
This article is Part 3 in a 3-Part Series.
Key Takeaways
- The scoring manifest covers 7,894 deduplicated images with 8,615 vetted ground truth values, all traced to BarBeR’s VIA annotations — the same manifest used by the C++ and Python runs.
- Each image is fetched once and decoded into an
ImageBitmap, then painted onto a shared canvas; only the decoder call is timed, so the fetch and bitmap-decode stage stays outside the clock. - On a single full-dataset run with DBR’s
ReadBarcodes_Defaulttemplate, Dynamsoft Barcode Reader reached 84.84% recall against 67.25% for zxing-wasm, reading 1,515 more of the 8,615 ground truth barcodes. - zxing-wasm held a slight precision edge (92.35% vs. 91.81%) and a faster median decode time (74.10 ms vs. 100.20 ms), so the trade-off is recall and tail latency versus a free, inspectable stack.
- The run reuses the series manifest, so these numbers can be diffed directly against the C++ (ZXing-C++ 3.1.0, DBR 11.4.20.7177) and Python (ZXing-C++ 3.1.1, DBR 11.6.10.8373) results.
Common Developer Questions
What is a good zxing-wasm alternative for difficult barcodes?
Dynamsoft Barcode Reader is the stronger alternative in this benchmark when images are damaged, crowded, low-contrast, or mixed-symbology. On the audited BarBeR set it read 7,309 of 8,615 ground truth instances for 84.84% recall, versus 5,794 instances and 67.25% recall for zxing-wasm — a lead of 1,515 barcodes and 17.59 percentage points. The gap is largest exactly where “difficult” lives: on CODE_128 it read 882 of 1,143 against zxing-wasm’s 361, and on EAN_13 it read 4,728 of 4,857 against 3,814. zxing-wasm remains a solid free baseline for clean, controlled input.
When should I replace ZXing with a commercial barcode SDK?
Replace ZXing when the cost of a missed scan outweighs the license fee — production workflows where unread barcodes mean manual rework, failed checkouts, or lost parcels. In this run zxing-wasm left 2,231 ground truth barcodes unread (not_found) against Dynamsoft’s 635, and its P95 decode time was 409.10 ms versus 313.80 ms, so difficult images are both less likely to decode and slower when they do. Stay on ZXing when your input is controlled and clean, you need inspectable open source with no licensing, and a ~67% read rate on hard images is acceptable.
How do I migrate from zxing-wasm to Dynamsoft Barcode Reader?
Swap the decode call and keep the rest of your pipeline. Where zxing-wasm takes an ImageData or Blob and returns {format, text} objects, Dynamsoft uses a CaptureVisionRouter that accepts a canvas, image, or ImageData and returns barcode items with formatString and text. The migration section below shows both calls side by side; the format names map almost one-to-one (EAN13 → EAN_13, QRCode → QR_CODE), and the payload text is directly comparable, so existing result-handling code carries over with a small format-normalization step.
Do the JavaScript results match the C++ and Python benchmarks on the same images?
Broadly yes on accuracy, with version- and runtime-driven deltas. On the identical manifest, the JavaScript DBR bundle (engine 11.4.3000) reached 84.84% recall, against 86.41% for the C++ run (11.4.20.7177) and 86.78% for the Python run (11.6.10.8373). zxing-wasm 3.1.2 in the browser scored 67.25% recall, against 67.96% for ZXing-C++ 3.1.0 in C++ and 67.43% for 3.1.1 in Python. The open-source engine is consistent across bindings; the DBR number moves with the SDK release more than with the language.
Full BarBeR Benchmark Video
The recording walks through the shared input path, the per-image timing, and the final accuracy and speed numbers.
Prerequisites
- Node.js 18 or later (developed on Node 24) for the collector and static/image server
- A modern browser (Chrome, Edge, or Firefox) to run the WASM decoders
- The BarBeR dataset images and the audited series manifest
- zxing-wasm 3.1.2 and dynamsoft-barcode-reader-bundle 11.4.3000, both loaded from pinned CDN builds
- A valid Dynamsoft license key. Get a 30-day free trial license.
Start the collector/static server, pointing it at your BarBeR images:
node src/server.mjs `
--images "D:/images/public-barcode-dataset/BarBeR - Dataset/dataset/images" `
--manifest manifests/benchmark_manifest.jsonl `
--output results/full `
--port 8790
Then open http://localhost:8790/, click Initialize SDKs, and click Run benchmark.
Step 1: Reuse the Audited BarBeR Manifest
This project does not re-audit the dataset. It loads the exact benchmark_manifest.jsonl produced by the series audit — 7,894 unique images carrying 8,615 reliable barcode instances, traced to BarBeR’s VIA annotations after dropping 853 images without reliable payload ground truth and one byte-identical duplicate. Reusing the manifest is what makes the three articles one controlled experiment: the C++, Python, and JavaScript runs all score against the same images, the same ground truth, and the same matching rules.
The server reads the manifest, hashes it, and streams each referenced image to the browser on request. The audit trail (source annotation hashes, exclusion lists, duplicate hits) ships in manifests/barber_source_files.json.
Step 2: Decode Images with Both JavaScript Readers
Each image is fetched once and decoded into an ImageBitmap, then painted onto a shared canvas. That stage is recorded separately as image_load_ns and never enters the decode statistics. Both readers then read the same pixels.
zxing-wasm takes an ImageData built from the canvas:
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const results = await ZXingWASM.readBarcodes(imageData);
for (const r of results) {
const text = r.text; // decoded payload
const format = r.format; // e.g. "EAN13", "QRCode", "Code128"
}
Dynamsoft Barcode Reader is reached through the Capture Vision router’s barcode preset, reading the same canvas:
await Dynamsoft.License.LicenseManager.initLicense(licenseKey, true);
await Dynamsoft.Core.CoreModule.loadWasm(["DBR"]);
const cvRouter = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
const captured = await cvRouter.capture(
canvas,
Dynamsoft.CVR.EnumPresetTemplate.PT_READ_BARCODES
);
for (const item of captured.getItems()) {
const text = item.text; // decoded payload
const format = item.formatString; // e.g. "EAN_13", "QR_CODE", "CODE_128"
}
Only the reader call is timed, recorded as decode_ns. Decoder order is shuffled per (sample, repetition) with a seeded PRNG so neither side gets a systematic warm-up advantage.
Step 3: Run the Benchmark
The browser walks the manifest and POSTs each per-image record to the Node collector, which scores it against the manifest ground truth and appends it to results.jsonl. The append-only stream keeps long runs resumable: the collector tracks which (sample, decoder, repetition) records already exist, so an interrupted pass picks up where it stopped instead of re-decoding.
Each decoder made one timed pass over all 7,894 images, and accuracy is scored against the 8,615 ground truth instances. Test machine: Windows 11, a 13th-generation Intel Core i5-13400F with 16 logical processors, Node v24.13.0 driving Chrome 150, and one benchmark process.
Step 4: Validate Results
Validation covers the record schema, image and ground truth counts, repetition consistency, summary totals, and explicit decoder errors.
node tools/validate_results.mjs `
--results results/full/results.jsonl `
--summary results/full/summary.json `
--expected-images 7894 `
--expected-ground-truth 8615 `
--expected-repetitions 1
Five BarBeR images use a JPEG encoding the browser’s createImageBitmap cannot decode; both readers receive the same explicit input_pipeline_error for those images, so the comparison stays fair and the recall denominator is unchanged. This is a browser image-decode limitation, not a decoder limitation — the native C++ and Python runs decoded these files through OpenCV.
Step 5: Inspect Accuracy and Decode Time
| Decoder | Correct | Recall | Precision | Image all-read rate | Mean decode time | Median decode time | P95 decode time |
|---|---|---|---|---|---|---|---|
| Dynamsoft Barcode Reader 11.4.3000 | 7,309 / 8,615 | 84.84% | 91.81% | 84.94% | 127.00 ms | 100.20 ms | 313.80 ms |
| zxing-wasm 3.1.2 | 5,794 / 8,615 | 67.25% | 92.35% | 67.03% | 121.75 ms | 74.10 ms | 409.10 ms |
The headline gap is recall: Dynamsoft Barcode Reader matched 1,515 more ground truth instances than zxing-wasm in this run, a lead of 17.59 percentage points. zxing-wasm answered with 0.54 percentage points better precision and a faster median decode time, but its P95 decode time was 95.30 ms slower, so difficult images are both less likely to decode and slower when they do. Treat the numbers as a measured trade-off on this dataset, not a universal ranking for every barcode workload.
Scoring normalizes UPC-A to its zero-prefixed EAN-13 equivalent first, and folds DBR’s CODE39EXTENDED results into CODE_39 whenever the payload matches. The per-format breakdown shows where the recall gap comes from:
| Format | Dynamsoft correct / eligible | zxing-wasm correct / eligible |
|---|---|---|
| EAN_13 | 4,728 / 4,857 | 3,814 / 4,857 |
| CODE_128 | 882 / 1,143 | 361 / 1,143 |
| QR_CODE | 1,074 / 1,339 | 1,081 / 1,339 |
| UPC_A | 311 / 319 | 257 / 319 |
| PDF_417 | 41 / 189 | 24 / 189 |
| DATA_MATRIX | 101 / 140 | 98 / 140 |
| CODE_39 | 39 / 202 | 29 / 202 |
| ITF | 55 / 73 | 58 / 73 |
Disclosure: Dynamsoft, the developer of Dynamsoft Barcode Reader, built and published this benchmark. BarBeR itself is an independent public dataset whose standardized annotations were produced with help from proprietary Datalogic software. Source hashes, exclusion lists, configurations, raw records, and the generated report all ship with the project so anyone can re-check the comparison.
The generated HTML report embeds searchable per-image records, and its download directory carries the full JSONL stream, the combined JSON package, the summary, and the source inventory.

node tools/generate_html_report.mjs `
--inventory manifests/barber_source_files.json `
--environment configs/benchmark_environment.json `
--results results/full/results.jsonl `
--results-json results/full/results.json `
--summary results/full/summary.json `
--output report/index.html
JavaScript vs. C++ and Python: Same BarBeR Images, Different Runtimes
The C++ and Python articles ran the identical 7,894-image BarBeR manifest. This JavaScript project reuses that manifest — same image paths, same SHA-256 hashes — so the three runs isolate how much the SDK release and the language/runtime binding move measured results on unchanged input.
| Decoder | Run | Correct | Recall | Precision | Mean decode time |
|---|---|---|---|---|---|
| Dynamsoft Barcode Reader | C++ 11.4.20.7177 | 7,444 / 8,615 | 86.41% | 91.44% | 70.08 ms |
| Dynamsoft Barcode Reader | Python 11.6.10.8373 | 7,476 / 8,615 | 86.78% | 91.49% | 63.84 ms |
| Dynamsoft Barcode Reader | JavaScript 11.4.3000 | 7,309 / 8,615 | 84.84% | 91.81% | 127.00 ms |
| ZXing-C++ | C++ 3.1.0 | 5,855 / 8,615 | 67.96% | 93.17% | 74.09 ms |
| ZXing-C++ | Python 3.1.1 | 5,809 / 8,615 | 67.43% | 92.35% | 70.22 ms |
| zxing-wasm | JavaScript 3.1.2 | 5,794 / 8,615 | 67.25% | 92.35% | 121.75 ms |
Two patterns stand out. The open-source ZXing core is consistent across all three bindings — recall stays within a point of 67–68% whether it runs as native C++, a Python wheel, or WebAssembly. The DBR number tracks the SDK release more than the language: the newer 11.6 engine in the Python run leads, and the 11.4-line engines in C++ and JavaScript cluster just behind. Decode times are not directly comparable across runtimes because the browser adds canvas and WASM boundary overhead that the native and OpenCV paths do not have.
Migrating from zxing-wasm to Dynamsoft Barcode Reader
The migration is a decode-call swap, not a pipeline rewrite. Both libraries accept the same browser image primitives and return plain payload text, so your result-handling, UI, and storage code carries over.
Before (zxing-wasm):
const results = await ZXingWASM.readBarcodes(imageData);
for (const r of results) {
handle(r.format, r.text);
}
After (Dynamsoft Barcode Reader):
await Dynamsoft.License.LicenseManager.initLicense(licenseKey, true);
await Dynamsoft.Core.CoreModule.loadWasm(["DBR"]);
const cvRouter = await Dynamsoft.CVR.CaptureVisionRouter.createInstance();
const captured = await cvRouter.capture(imageData, Dynamsoft.CVR.EnumPresetTemplate.PT_READ_BARCODES);
for (const item of captured.getItems()) {
handle(item.formatString, item.text);
}
Three practical notes:
- Format names map almost one-to-one. zxing-wasm returns
EAN13,QRCode,Code128,DataMatrix; Dynamsoft returnsEAN_13,QR_CODE,CODE_128,DATA_MATRIX. Normalize to a canonical form (uppercase, strip separators) before comparing, exactly as this benchmark’s scorer does. - Payload text is directly comparable. UPC-A may surface as its zero-prefixed EAN-13 equivalent on either side, so apply the same leading-zero normalization your application already needs.
- Initialization is a one-time await. Load the WASM module and create the router once at startup, then reuse the router for every frame or image — the per-call cost after that is just the decode.
Accuracy Metrics
Recall measures how completely a reader covers the barcodes that actually exist; precision measures how trustworthy its answers are when it does report one:
Recall = correct ground truth matches / eligible ground truth instances
Precision = correct predictions / evaluated predictions
Evaluated predictions = correct + wrong_text + wrong_format + extra_result
A not_found or unsupported_format outcome means a known ground truth value went unread, so it pulls recall down without touching precision — the decoder reported nothing for that item. A wrong_text, wrong_format, or extra_result outcome means the decoder reported something matching no ground truth barcode, so it pulls precision down.
Matching Rules
- Ground truth and predictions are matched one to one as multisets.
- The key is canonical barcode format plus exact normalized payload.
- UPC-A and the equivalent zero-prefixed EAN-13 value are treated as equal.
- DBR
CODE39EXTENDEDoutput is treated asCODE_39when the payload matches. - Barcode location is not part of the score.
- Unsupported formats remain visible in coverage-adjusted metrics.
- Decoder errors and input pipeline errors are explicit outcomes.
Common Issues & Edge Cases
- zxing-wasm rejects a canvas element: Version 3.x does not accept a canvas directly. Pass an
ImageData(fromctx.getImageData) or aBlob; this benchmark usesImageDataso both readers share the same pixels. - DBR fails during initialization: Supply a valid license through
LicenseManager.initLicense. Without a key the SDK falls back to a 24-hour public demo license. - A BarBeR image will not decode in the browser: A handful of BarBeR JPEGs use an encoding
createImageBitmapcannot decode. Both readers record an explicitinput_pipeline_errorfor those images, so the comparison stays fair; the native runs decoded them through OpenCV. - A run stops before completion: Reload the page and click Run again. The collector recognizes finished (sample, decoder, repetition) records and resumes past them instead of re-decoding.
When to Choose Dynamsoft Barcode Reader or zxing-wasm
If your web pipeline cannot afford missed reads, Dynamsoft Barcode Reader is the stronger fit in this benchmark: it returned 1,515 more correct matches on the audited BarBeR set, and that margin matters most on damaged labels, crowded scenes, and mixed-symbology captures — the difficult barcodes where zxing-wasm left 2,231 instances unread. It also brings configurable capture templates, packaged runtime assets, and commercial support, the pieces production deployments tend to need.
If you want an engine you can inspect, patch, and ship without license fees, zxing-wasm remains a solid option. Its 92.35% precision in this run shows its answers are trustworthy when it finds one, its median decode time was faster, and for controlled inputs where a lower read rate is acceptable, the open-source stack may be all you need. Because both readers load from a CDN and run in the same page, the decision comes down to recall tolerance and licensing rather than integration effort.
Conclusion
This project shows that a JavaScript benchmark can carry the same audit trail as the native and Python builds: the shared manifest, resumable JSONL records, combined JSON package, environment snapshot, and generated HTML report let any reviewer reproduce the accuracy and decode-time numbers above. Paired with the C++ and Python runs on the identical manifest, it also isolates how much the SDK release and the browser/WASM runtime move those numbers on unchanged input — and it doubles as a working migration reference for teams moving a web scanner from zxing-wasm to Dynamsoft Barcode Reader.