浏览器里动态加载 opencv.js:等 onRuntimeInitialized

想在网页里用 OpenCV.js 做图像处理,标准写法是:

<script async src="https://docs.opencv.org/4.x/opencv.js" onload="onOpenCvReady();"></script>

想改成 JS 动态加载(比如按需加载、或者写扩展 / 油猴脚本),有几个坑。

基础动态加载

const script = document.createElement("script");
script.src = "https://docs.opencv.org/4.x/opencv.js";
script.async = true;
script.onload = () => onOpenCvReady();
document.head.appendChild(script);

但这个 onload 触发时,OpenCV 还没准备好——window.cv 存在,但里面的 native 函数还没初始化(emscripten 编译的 WASM 需要额外时间)。

正确姿势:等 onRuntimeInitialized

script.onload = () => {
  cv.onRuntimeInitialized = () => {
    console.log("OpenCV Ready");
    onOpenCvReady();
  };
};

cv.onRuntimeInitialized 是 emscripten 的钩子,WASM 加载并 instantiate 完毕后调用。这时 cv.imreadcv.Mat 才能真正用

Promise 化封装

function loadOpenCV() {
  return new Promise((resolve, reject) => {
    // 已加载过
    if (window.cv && cv.Mat) return resolve(cv);

    // 加载中(多次调用不重复插 script)
    if (window.cv) {
      cv.onRuntimeInitialized = () => resolve(cv);
      return;
    }

    const script = document.createElement("script");
    script.src = "https://docs.opencv.org/4.x/opencv.js";
    script.async = true;
    script.onload = () => {
      cv.onRuntimeInitialized = () => resolve(cv);
    };
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// 用
loadOpenCV().then(cv => {
  const mat = new cv.Mat(100, 100, cv.CV_8UC3);
  console.log(mat);
});

关键点:

  • if (window.cv && cv.Mat) — 双重判断,cv 存在不代表 ready
  • 支持并发调用(多次 loadOpenCV() 只加载一次真正的 script)
  • 错误传给 reject(网络挂了能捕获)

油猴脚本里加载

油猴脚本里 document.createElement("script") 遇到有 CSP(Content-Security-Policy)的网站会失败——unsafe-inline 被禁的情况下,外链 script 加载被阻。

@require 声明,油猴会把脚本预先拉下来注入到你的沙箱里:

// ==UserScript==
// @name         Load OpenCV
// @match        *://*/*
// @require      https://docs.opencv.org/4.x/opencv.js
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  cv.onRuntimeInitialized = () => {
    console.log("OpenCV Ready");
    const mat = new cv.Mat(100, 100, cv.CV_8UC3);
    console.log(mat);
  };
})();

@require 走的是 Tampermonkey / Violentmonkey / ScriptCat 自己的加载机制,不走页面 CSP。这是给受 CSP 保护的网站注 OpenCV 最稳的方式

常见问题

1. cv is not defined

script 还没加载完就用 cv。用 Promise 版本,.then 里再用。

2. cv.imread is not a function

onRuntimeInitialized 没等——script.onload 触发早于 runtime init。

3. 用了 async/await 但还是异步崩溃

外面 async 函数忘了 await

async function process() {
  const cv = await loadOpenCV();       // 别忘 await
  const mat = new cv.Mat(...);
}

4. 内存泄漏

OpenCV.js 的 Mat 是 WASM 分配的内存,必须手动 delete

const mat = cv.imread("input");
try {
  // 处理 mat
} finally {
  mat.delete();
}

不 delete,跑几张图页面就 OOM。

想省点带宽

opencv.js 从官方 CDN 拉大约 8MB。有几个选择:

  • 换镜像https://cdn.jsdelivr.net/npm/@techstark/opencv-js@4.x.x/dist/opencv.js
  • 裁剪版本:自己 build 只带需要的模块,webnnvideoio 之类不需要就砍
  • 本地缓存:Service Worker 缓存 opencv.js,二次加载秒开
  • 动态判断需求:只有用户点了”图像处理”按钮再加载

第一次加载 8MB 是常态,别期望闪电般。

一句话总结

动态加载 opencv.js 要等 cv.onRuntimeInitialized,封成 Promise 用起来最顺。油猴脚本里遇到 CSP 就用 @require。用完 Mat 记得 delete()