作者為何讓瀏覽器內背景移除工具的 u2netp 以單執行緒執行:crossOriginIsolated 是取捨,不是勾選項
crossOriginIsolated is a tradeoff, not a checkbox - why my in-browser ML model runs single-threaded
作者的背景移除工具在頁面未啟用跨來源隔離時,會將 onnxruntime-web 的 WASM 執行緒數設為 1,讓 u2netp 以單執行緒執行。作者避免採用 COEP: require-corp,因為它可能阻擋沒有 CORP 標頭或 CORS 配置的跨來源資源,影響 OAuth 登入彈窗及第三方指令碼。
crossOriginIsolated is a tradeoff, not a checkbox — why my in-browser ML model runs single-threaded
If you run an ONNX model in the browser with onnxruntime-web, the multithreaded WASM build wants SharedArrayBuffer. And SharedArrayBuffer is only available when the page is cross-origin isolated, which means you've shipped both a COOP and a COEP header.
The tempting advice is "just enable cross-origin isolation." I want to show why we deliberately did not, in a real production app, and what the honest tradeoff looks like.
What we actually do
Our background-removal tool self-hosts a small model (u2netp) and runs it through onnxruntime-web's wasm bundle. The relevant part of our engine, verbatim:
const ort = (await import("onnxruntime-web/wasm")) as unknown as OrtModule;
if (!ortConfigured) {
ort.env.wasm.wasmPaths = BG_WASM_BASE;
// This site is not cross-origin isolated, so SharedArrayBuffer threading is
// unavailable; pin to a single thread to avoid a broken threaded init.
if (typeof globalThis.crossOriginIsolated === "undefined" || !globalThis.crossOriginIsolated) {
ort.env.wasm.numThreads = 1;
}
ort.env.wasm.proxy = false;
ortConfigured = true;
}
We detect isolation with globalThis.crossOriginIsolated and, when it's false, pin numThreads = 1 instead of letting a threaded init half-break. We also show a "compatibility mode" message rather than an error. The model just runs single-threaded.
Why not just turn on COOP/COEP?
Because COEP changes the rules for every cross-origin subresource on the page. With Cross-Origin-Embedder-Policy: require-corp, anything you load from another origin has to opt in with a Cross-Origin-Resource-Policy header (or CORS). If it doesn't, the browser blocks it — silently.
Our own decision log is blunt about this. The note next to the threading section reads:
Threading requires
Cross-Origin-Isolatedforwasm_threads. If not isolated, gracefully fall back to single-thread WASM (feature detect, show "compatibility mode"). Do not requireCOOP/COEPheaders in MVP — they break OAuth/ads.
require-corp would have broken our OAuth sign-in popup and the third-party scripts we legitimately load, all to win threading on one optional, occasionally-used tool. For a privacy-first tool where the headline is "your file never leaves the tab," making the whole page's resource loading more fragile to buy some inference speed on the heaviest feature was not a good trade. The small model + single-thread fallback was the honest engineering call.
I want to be equally honest about what I didn't do: I have no measured, per-device speed numbers for single- vs multi-thread on our exact model. The decision was structural (does isolation break the things I need?), not a benchmark win I can quote.
The third option I didn't know about
This came up in a thread with @szp2005, who runs a heavier matting model in the browser. When I described our not-isolated, single-threaded setup, they pointed out that we weren't limited to the binary "isolation vs. broken analytics" I'd assumed. Their words:
We do isolate, using
COEP: credentiallessinstead ofrequire-corp. Safari ignores it and stays single-threaded.
That reframed the whole choice for me. credentialless lets cross-origin subresources load without credentials and without each resource sending a CORP header — so you get the isolation (and the threaded WASM) without forcing every third-party embed to opt in first. The exact embed-blocking that made require-corp unusable for us largely stops applying.
And the Safari caveat is the part that actually matters. If a browser ignores credentialless, then cross-origin isolation is a progressive enhancement, not a guarantee. You cannot design the happy path assuming isolation is on, because there is always an engine where it silently isn't. Which is exactly why our numThreads = 1 fallback and their "timeout retry had to start a new Worker" both have to exist regardless of which COEP mode you ship.
Takeaways
- Treat
crossOriginIsolatedas a capability you feature-detect, never a precondition you assume. Ship the single-threaded path first. -
require-corpis a page-wide contract with every third-party you load; that's a big blast radius for a speed win on one feature. -
COEP: credentiallessis worth testing specifically because its failure mode (blocking non-CORP embeds) is the one that usually disqualifies COEP — but plan for the engines that ignore it. - The right question isn't "how do I get multithreaded WASM," it's "what does turning on isolation break, and is the thing I gain worth it."
If you've wrestled with credentialless vs require-corp in a real app, I'd genuinely like to hear where it held and where it didn't.
Disclosure: drafted with AI assistance from my own source code and the linked conversation; every code excerpt and quoted claim was checked against the actual repository before publishing. I wrote and verified the content.
來源:DEV Community · dev.to