perf: load the QR libraries only for device verification (bundle budget)
CI / Build & Quality Checks (push) Successful in 2m0s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 7s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 10m51s
CI / Build & Quality Checks (push) Successful in 2m0s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 7s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 10m51s
The main chunk had crept to 356.9 kB gzip against a 349.9 kB budget (a hard gate on pull requests, only a warning on pushes, so it went unnoticed until PR #241). jsQR (camera scanning) and qrcode (drawing the QR) were statically imported by the device-verification components, so every startup loaded them. Both are now dynamic imports: jsQR when the scanner opens (fetched in parallel with the camera permission), qrcode when a QR is drawn. Main chunk 356.9 → 302.5 kB gzip (largest chunk is now matrix-sdk at 304.7 kB, within budget); chromium e2e 19 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
27c6dacc60
commit
6bb3f2af27
@@ -8,7 +8,6 @@ import {
|
|||||||
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { VerificationMethod } from 'matrix-js-sdk/lib/types';
|
import { VerificationMethod } from 'matrix-js-sdk/lib/types';
|
||||||
import QRCode from 'qrcode';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -221,12 +220,16 @@ function QrCodeImage({ data }: { data: Uint8ClampedArray }) {
|
|||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
// Byte-mode so the raw verification bytes round-trip (a string value would
|
// Byte-mode so the raw verification bytes round-trip (a string value would
|
||||||
// mangle high bytes via UTF-8).
|
// mangle high bytes via UTF-8). The QR library is loaded on demand.
|
||||||
QRCode.toCanvas(canvas, [{ data: new Uint8Array(data), mode: 'byte' }], {
|
import('qrcode')
|
||||||
width: 220,
|
.then(({ default: QRCode }) =>
|
||||||
margin: 2,
|
QRCode.toCanvas(canvas, [{ data: new Uint8Array(data), mode: 'byte' }], {
|
||||||
color: { dark: '#000000', light: '#ffffff' },
|
width: 220,
|
||||||
}).catch(() => undefined);
|
margin: 2,
|
||||||
|
color: { dark: '#000000', light: '#ffffff' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch(() => undefined);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
return (
|
return (
|
||||||
<Box justifyContent="Center">
|
<Box justifyContent="Center">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Box, Button, color, config, Text } from 'folds';
|
import { Box, Button, color, config, Text } from 'folds';
|
||||||
import jsQR from 'jsqr';
|
|
||||||
|
|
||||||
type QrScannerProps = {
|
type QrScannerProps = {
|
||||||
onScan: (bytes: Uint8ClampedArray) => void;
|
onScan: (bytes: Uint8ClampedArray) => void;
|
||||||
@@ -10,6 +9,7 @@ type QrScannerProps = {
|
|||||||
// Camera QR scanner. Decodes frames with jsQR and hands back the raw byte
|
// Camera QR scanner. Decodes frames with jsQR and hands back the raw byte
|
||||||
// segment (`result.binaryData`) — Matrix QR verification needs the raw bytes,
|
// segment (`result.binaryData`) — Matrix QR verification needs the raw bytes,
|
||||||
// not a decoded string, so the string-only `BarcodeDetector` can't be used.
|
// 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) {
|
export function QrScanner({ onScan, onCancel }: QrScannerProps) {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
@@ -20,15 +20,22 @@ export function QrScanner({ onScan, onCancel }: QrScannerProps) {
|
|||||||
let raf = 0;
|
let raf = 0;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
let decode: typeof import('jsqr').default | undefined;
|
||||||
|
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
const video = videoRef.current;
|
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.width = video.videoWidth;
|
||||||
canvas.height = video.videoHeight;
|
canvas.height = video.videoHeight;
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
const image = ctx.getImageData(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) {
|
if (result && result.binaryData.length > 0) {
|
||||||
doneRef.current = true;
|
doneRef.current = true;
|
||||||
onScan(new Uint8ClampedArray(result.binaryData));
|
onScan(new Uint8ClampedArray(result.binaryData));
|
||||||
@@ -40,9 +47,12 @@ export function QrScanner({ onScan, onCancel }: QrScannerProps) {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
stream = await navigator.mediaDevices.getUserMedia({
|
const [lib, media] = await Promise.all([
|
||||||
video: { facingMode: 'environment' },
|
import('jsqr'),
|
||||||
});
|
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }),
|
||||||
|
]);
|
||||||
|
decode = lib.default;
|
||||||
|
stream = media;
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.srcObject = stream;
|
videoRef.current.srcObject = stream;
|
||||||
await videoRef.current.play();
|
await videoRef.current.play();
|
||||||
|
|||||||
Reference in New Issue
Block a user