diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx index 758c95c7f..aa9fa1f48 100644 --- a/src/app/components/DeviceVerification.tsx +++ b/src/app/components/DeviceVerification.tsx @@ -8,7 +8,6 @@ import { import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { VerificationMethod } from 'matrix-js-sdk/lib/types'; -import QRCode from 'qrcode'; import { Box, Button, @@ -221,12 +220,16 @@ function QrCodeImage({ data }: { data: Uint8ClampedArray }) { const canvas = canvasRef.current; if (!canvas) return; // Byte-mode so the raw verification bytes round-trip (a string value would - // mangle high bytes via UTF-8). - QRCode.toCanvas(canvas, [{ data: new Uint8Array(data), mode: 'byte' }], { - width: 220, - margin: 2, - color: { dark: '#000000', light: '#ffffff' }, - }).catch(() => undefined); + // mangle high bytes via UTF-8). The QR library is loaded on demand. + import('qrcode') + .then(({ default: QRCode }) => + QRCode.toCanvas(canvas, [{ data: new Uint8Array(data), mode: 'byte' }], { + width: 220, + margin: 2, + color: { dark: '#000000', light: '#ffffff' }, + }), + ) + .catch(() => undefined); }, [data]); return ( diff --git a/src/app/components/QrScanner.tsx b/src/app/components/QrScanner.tsx index 6a4d68da6..f3ebb2d11 100644 --- a/src/app/components/QrScanner.tsx +++ b/src/app/components/QrScanner.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; import { Box, Button, color, config, Text } from 'folds'; -import jsQR from 'jsqr'; type QrScannerProps = { onScan: (bytes: Uint8ClampedArray) => void; @@ -10,6 +9,7 @@ type QrScannerProps = { // Camera QR scanner. Decodes frames with jsQR and hands back the raw byte // segment (`result.binaryData`) — Matrix QR verification needs the raw bytes, // not a decoded string, so the string-only `BarcodeDetector` can't be used. +// jsQR is loaded on demand: it is large and only needed while scanning. export function QrScanner({ onScan, onCancel }: QrScannerProps) { const videoRef = useRef(null); const [error, setError] = useState(); @@ -20,15 +20,22 @@ export function QrScanner({ onScan, onCancel }: QrScannerProps) { let raf = 0; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d', { willReadFrequently: true }); + let decode: typeof import('jsqr').default | undefined; const tick = () => { const video = videoRef.current; - if (!doneRef.current && video && ctx && video.readyState === video.HAVE_ENOUGH_DATA) { + if ( + decode && + !doneRef.current && + video && + ctx && + video.readyState === video.HAVE_ENOUGH_DATA + ) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const image = ctx.getImageData(0, 0, canvas.width, canvas.height); - const result = jsQR(image.data, image.width, image.height); + const result = decode(image.data, image.width, image.height); if (result && result.binaryData.length > 0) { doneRef.current = true; onScan(new Uint8ClampedArray(result.binaryData)); @@ -40,9 +47,12 @@ export function QrScanner({ onScan, onCancel }: QrScannerProps) { (async () => { try { - stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'environment' }, - }); + const [lib, media] = await Promise.all([ + import('jsqr'), + navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }), + ]); + decode = lib.default; + stream = media; if (videoRef.current) { videoRef.current.srcObject = stream; await videoRef.current.play();