Quick Links

QR codes have become much more popular in recent years. They're widely used to deliver information and facilitate check-ins. While most commonly found as part of native mobile apps, you can also incorporate QR scans into your websites.

Here's how to use the popular jsQR library in combination with Web Workers to offer a high-performance QR scanning experience on the web. We'll assume you're already familiar with JavaScript basics and you've got a functioning site which you're ready to add your code to.

1. Getting Started

Begin by downloading the jsQR library in pre-compiled distributable format. Make sure it's publicly accessible on your web server; in this article, we're assuming the URL is

        /jsQR.js
    

. jsQR is also available as an npm package if you've got a build routine configured.

This package provides real-time detection of QR codes visible in a video stream. It retrieves the data within the code and supplies it to your application.

2. Getting a Camera Feed

The next step is to acquire a MediaStream from the browser. Use the

        mediaDevices
    

API to get a new stream from the user's camera:

const getCamera = async () => {

if (!navigator.mediaDevices) {

throw new Error("mediaDevices API unavailable.");

}

const devices = await navigator.mediaDevices.enumerateDevices();

const cameras = devices.filter(d => (d.kind === "videoinput"));

return cameras[0];

};

We're selecting the first camera which is found. You could add extra logic to enable selection of a specific camera indicated by the user. In a real app, you should also improve the error handling when the

        mediaDevices
    

API is unavailable. This could be because the user is in an old web browser or they've permanently blocked the camera access permission.

Use the

        getCamera()
    

function to acquire the video stream. Attach the stream to a

        <video>
    

element so it can be played and displayed to the user:

<video 

autoplay="true"

id="video"

muted="true" />

const stream = await getCamera();

const video = document.getElementById("video");

video.srcObject = stream;

await video.play();

3. Creating a Canvas

The next step is to create a

        canvas
    

element. The video data will be streamed from the camera onto the

        canvas
    

where the image pixels will be extracted and fed to jsQR. The canvas needs to be sized to match the dimensions of the video stream.

const videoTracks = await stream.getVideoTracks();

const videoTrackSettings = videoTracks[0].getSettings();

const canvas = document.createElement("canvas");

canvas.height = videoTrackSettings.height;

canvas.width = videoTrackSettings.width;

const canvasContext = canvas.getContext("2d");

The canvas context will be used in the next step to draw each frame of the video stream onto the canvas.

4. Adding jsQR

Add jsQR to your code next. Running jsQR in a web worker lets the browser delegate it to a background process, improving performance. jsQR needs to scan every video frame so running it in your main JavaScript thread can incur severe slowdowns on low-end devices.

The main thread and your web worker can communicate using messages. Your main thread will message the jsQR worker each time a new video frame is available. The worker will inspect that frame for QR codes in its background thread, then send a message to the main thread when the frame has been processed.

Load your worker in your main JavaScript code and add a message listener:

const qrWorker = new Worker("/qr-worker.js");

qrWorker.addEventListener("message", ({data}) => {

if (data) {

// Data from QR code available

//

// Handle a successful scan here.

}

else {

// No QR code detected in this frame

//

// Feed the next frame to the QR worker

// now (this code is introduced below).

tick();

}

});

Next add

        qr-worker.js
    

to your project. This needs to be publicly accessible at the URL given to the

        Worker
    

constructor. Browsers will download the web worker at the time it's needed.

importScripts("jsQR.js");

self.addEventListener("message", e => {

const {data, width, height} = e.data;

const qrData = jsQR(data, width, height);

self.postMessage(qrData);

});

The

        importScripts()
    

function downloads the jsQR library. This call is equivalent to a

        <script>
    

tag in your HTML but makes the script's contents available to your web worker.

A message listener is added to receive events from your JavaScript's main thread. Each event needs to include the data, width, and height of a video frame streamed from the camera. These values are fed to jsQR which will attempt to detect a QR code within the frame. The result is posted back to the main thread.

5. Wiring Everything Up

The final step is to create an update loop that feeds video frames to the jsQR worker periodically.

const updateJsQr = () => {

canvasContext.drawImage(video, 0, 0, canvas.width, canvas.height);

const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);

qrWorker.postMessage({data: imageData, height: canvas.height, width: canvas.width});

}

const tick = () => requestAnimationFrame(updateJsQr);

tick();

The

        tick()
    

function requests the browser allocate time to calling

        updateJsQr()
    

before the next repaint. This function gets the current video frame, draws it to the canvas, and then pulls out the resulting image data ready to pass to jsQR. The

        postMessage()
    

method on the worker instance actually sends the data.

The worker will then act on the data as shown in the code above. The detection result is communicated back to the main thread which processes it as shown in Step 4. When a QR code is detected, your application should use the retrieved data to move forward in its flow. When there's no detected code,

        tick()
    

is called again to pass the next frame to the QR worker, ready for assessment.

This mechanism ensures you don't end up overloading the browser by running multiple frame detections concurrently. Each call to

        jsQR()
    

is potentially expensive so you want to wait until the result is known before scheduling another. The loop is only sustained until a successful result is obtained, with jsQR handling one frame at a time. Your site's UI and its real-time camera preview will remain responsive throughout as the worker runs independently in the background.

6. jsQR Result Data

Each successful call to

        jsQR()
    

returns a result object with the following properties:

  •         data
        
    - String data extracted from the QR code (this is the only property used in the example above).
  •         binaryData
        
    - A
            Uint8ClampedArray
        
    containing the raw bytes read from the QR code.
  •         version
        
    - Detected QR code version.
  •         location
        
    - An object that describes the position of the detected QR code within the video frame, including points for each corner.

No data is available when jsQR fails to detect a code in the provided image.

7. Conclusion

Combining jsQR with Web Workers lets you implement high performance QR code scans in your web application. QR codes are an easy and convenient data sharing mechanism which most mobile users are now familiar with.

Although you could use jsQR as part of your main loop, you'd need to implement aggressive throttling to keep your UI responsive. This would result in a longer delay before a successful detection. Web Workers are supported in all modern browsers and provide a substantial performance boost to this solution, enabling split-second scans under optimal lighting conditions.