Lazy load open cv (#6236)

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
Reece Browne
2026-04-28 17:36:34 +01:00
committed by GitHub
parent 4e4918b91e
commit a26b15b1fe
3 changed files with 141 additions and 69 deletions

View File

@@ -19,8 +19,5 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.tsx"></script>
<!-- jscanify and OpenCV for mobile scanner - loaded after React for non-blocking page load -->
<script src="/vendor/jscanify/opencv.js" async></script>
<script src="/vendor/jscanify/jscanify.js" async></script>
</body>
</html>

View File

@@ -20,14 +20,7 @@ import PhotoCameraRoundedIcon from "@mui/icons-material/PhotoCameraRounded";
import UploadRoundedIcon from "@mui/icons-material/UploadRounded";
import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded";
import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded";
// jscanify is loaded via script tag in index.html as a global
declare global {
interface Window {
jscanify: any;
cv: any;
}
}
import { loadJscanify } from "@app/utils/loadJscanify";
/**
* MobileScannerPage
@@ -136,69 +129,36 @@ export default function MobileScannerPage() {
validateSession();
}, [sessionId, t]);
// Initialize jscanify scanner and wait for OpenCV (loaded via script tags in index.html)
useEffect(() => {
let retryCount = 0;
const MAX_RETRIES = 50; // 5 seconds max wait
let cancelled = false;
const initScanner = () => {
// Check if both OpenCV and jscanify are loaded
if (!(window as any).cv || !(window as any).cv.Mat) {
retryCount++;
if (retryCount < MAX_RETRIES) {
if (retryCount % 10 === 1) {
setLoadingStatus(
`Loading OpenCV... (${retryCount}/${MAX_RETRIES})`,
);
console.log(
`[${retryCount}/${MAX_RETRIES}] Waiting for OpenCV to load...`,
);
}
setTimeout(initScanner, 100);
} else {
const error =
"OpenCV failed to load after 5 seconds. Check that /vendor/jscanify/opencv.js is accessible.";
setLoadingStatus("OpenCV load failed ✗");
console.error(error);
loadJscanify({
onStatus: (status) => {
if (!cancelled) setLoadingStatus(status);
},
})
.then(() => {
if (cancelled) return;
try {
scannerRef.current = new window.jscanify!();
setOpenCvReady(true);
console.log("✓ jscanify initialized with OpenCV");
} catch (err) {
setLoadingStatus("jscanify init failed ✗");
console.error("Failed to initialize jscanify:", err);
}
return;
}
})
.catch((err) => {
if (cancelled) return;
setLoadingStatus(
`Scanner library failed to load ✗: ${(err as Error).message}`,
);
console.error("Failed to load jscanify:", err);
});
if (!window.jscanify) {
retryCount++;
if (retryCount < MAX_RETRIES) {
if (retryCount % 10 === 1) {
setLoadingStatus(
`Loading jscanify... (${retryCount}/${MAX_RETRIES})`,
);
console.log(
`[${retryCount}/${MAX_RETRIES}] Waiting for jscanify to load...`,
);
}
setTimeout(initScanner, 100);
} else {
const error =
"jscanify failed to load after 5 seconds. Check that /vendor/jscanify/jscanify.js is accessible.";
setLoadingStatus("jscanify load failed ✗");
console.error(error);
}
return;
}
try {
scannerRef.current = new window.jscanify();
setOpenCvReady(true);
// Don't set status here - let camera/detection effects control status from now on
console.log("✓ jscanify initialized with OpenCV");
} catch (err) {
setLoadingStatus("jscanify init failed ✗");
console.error("Failed to initialize jscanify:", err);
}
return () => {
cancelled = true;
};
// Start initialization
setLoadingStatus("Loading OpenCV...");
initScanner();
}, []);
// Initialize camera

View File

@@ -0,0 +1,115 @@
declare global {
interface Window {
cv?: any;
jscanify?: any;
}
}
const OPENCV_SRC = "/vendor/jscanify/opencv.js";
const JSCANIFY_SRC = "/vendor/jscanify/jscanify.js";
let loadPromise: Promise<void> | null = null;
function injectScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
// data-loaded: load event fired. data-loading: in flight.
// Neither: foreign tag (e.g. HMR-preserved); trust the downstream
// global check rather than wait on a load event that may already
// have dispatched.
const existing = document.querySelector<HTMLScriptElement>(
`script[src="${src}"]`,
);
if (existing) {
if (existing.dataset.loaded === "true") {
resolve();
return;
}
if (existing.dataset.loading === "true") {
existing.addEventListener("load", () => resolve(), { once: true });
existing.addEventListener(
"error",
() => reject(new Error(`Failed to load ${src}`)),
{ once: true },
);
return;
}
resolve();
return;
}
const script = document.createElement("script");
script.src = src;
script.async = true;
script.dataset.loading = "true";
script.onload = () => {
script.dataset.loaded = "true";
delete script.dataset.loading;
resolve();
};
script.onerror = () => {
delete script.dataset.loading;
reject(new Error(`Failed to load ${src}`));
};
document.head.appendChild(script);
});
}
function waitForGlobal(
check: () => boolean,
name: string,
timeoutMs: number,
onProgress?: (elapsedMs: number) => void,
): Promise<void> {
return new Promise((resolve, reject) => {
const start = Date.now();
const tick = () => {
if (check()) {
resolve();
return;
}
const elapsed = Date.now() - start;
if (elapsed > timeoutMs) {
reject(new Error(`Timed out waiting for ${name}`));
return;
}
onProgress?.(elapsed);
setTimeout(tick, 100);
};
tick();
});
}
export interface LoadJscanifyOptions {
onStatus?: (status: string) => void;
}
export function loadJscanify(options: LoadJscanifyOptions = {}): Promise<void> {
const { onStatus } = options;
if (loadPromise) return loadPromise;
loadPromise = (async () => {
onStatus?.("Loading OpenCV...");
await injectScript(OPENCV_SRC);
// OpenCV's script load event fires before the WASM runtime is ready.
await waitForGlobal(
() => !!window.cv && !!window.cv.Mat,
"OpenCV runtime (cv.Mat)",
15000,
(elapsed) => {
if (elapsed > 2000) onStatus?.("Initializing OpenCV runtime...");
},
);
onStatus?.("Loading jscanify...");
await injectScript(JSCANIFY_SRC);
await waitForGlobal(() => !!window.jscanify, "jscanify global", 5000);
onStatus?.("Scanner ready");
})().catch((err) => {
loadPromise = null;
throw err;
});
return loadPromise;
}