cd /news/developer-tools/how-enabling-cross-origin-isolation-… · home topics developer-tools article
[ARTICLE · art-121354] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

How enabling cross-origin isolation silently broke our multi-threaded WASM image compressor

A developer's browser-side image compressor, built with Rust compiled to WASM and WebGPU, crashed for every format after enabling cross-origin isolation in production. The root cause was that the threaded WASM package, which spawns nested rayon workers, was automatically loaded when crossOriginIsolated was true, but those workers were blocked by the Cross-Origin-Embedder-Policy (COEP) require-corp header. The fix decoupled the threaded package from COI availability, defaulted to a single-threaded path, and added self-healing on worker crashes.

read4 min views1 publishedSep 4, 2026

A production postmortem. We shipped browser-side image compression (Rust → WASM + WebGPU), turned on cross-origin isolation for speed, and watched every format crash with compression worker crashed. Here's the root cause and the fix.

We built an image compressor that runs 100% in the browser — Rust compiled to WASM for the codec work, WebGPU for the heavy ML passes (background removal, denoise, watermark). No upload, so users' pixels never leave the device. Privacy is the whole selling point.

For the multi-threaded code paths we rely on shared memory + atomics, which in the browser requires crossOriginIsolated

. So we served the document with:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

That gives us crossOriginIsolated === true

, unlocks SharedArrayBuffer

, and lets the *‑threaded

WASM builds actually spawn workers.

The build uses a nightly toolchain (nightly-2025-06-01

  • -Z build-std

) with:

RUSTFLAGS="--cfg=... +atomics,+bulk-memory --shared-memory --import-memory"

and a custom rayon handle pool (with_turbo_pool

) instead of build_global

, so we control worker lifecycle and can abort/self-heal.

After flipping COEP to require-corp

in production, every format started crashing with the same message:

compression worker crashed

Not one codec — JPG, PNG, WebP, AVIF, all of them. It was a P0: the core feature was dead for every user.

What made it nasty: it only reproduced under real cross-origin isolation. Local dev without COEP was fine. Staging without the header was fine. So the bug hid until it hit production traffic.

The *‑threaded

WASM packages spin up nested rayon workers to parallelize the codec. Under COI + COEP require-corp

, those nested workers get blocked by Cross-Origin-Resource-Policy

/ COEP — the spawned worker script is treated as a cross-origin response without the right CORP header, so the browser refuses it. No worker → the rayon pool never initializes → the compression call throws compression worker crashed

.

The trap: we had gated the ‑threaded build on crossOriginIsolated

:

// ❌ the bug: environment detection == feature enable
if (crossOriginIsolated) {
  loadThreadedPackage();   // boots nested rayon workers → blocked by COEP
}

So enabling COI auto-enabled the broken path. COI was supposed to be the enabler, but it also activated the exact code that COEP then killed. A perfect deadlock between two headers we set ourselves.

(This matches the fix commits 569731d

"默认压缩路径禁用 *-threaded 线程包" and 73e9377

"COI 下 module worker 被 COEP 拦死".)

Three changes, all in the — not the codec:

1. Decouple "COI is available" from "use threads."

We no longer load *‑threaded

just because crossOriginIsolated

is true. It loads only on an explicit opt-in — our "Turbo" toggle (v2.compressAccel

):

// ✅ capability != automatic enable
const wantsTurbo = settings.compressAccel === true; // user/plan explicit
if (wantsTurbo && crossOriginIsolated) {
  loadThreadedPackage();
}
// default path stays single-threaded and COEP-safe

2. Keep the single-threaded path as the default and make it robust.

The non-threaded wasm (e.g. ORT 1.17 legacy, which ships a real single-thread build) works fine under COEP. Defaulting to it means COI no longer breaks the common case.

3. Self-heal on worker crash.

pool.ts

now catches a dead worker and retries on the safe path, and aborts Turbo multi-core rather than bubbling a hard crash:

worker.oncrash = () => {
  retryOnSingleThread();   // f8c86d5: 自愈重试覆盖 JPG/PNG/WEBP
  disableTurbo();          // fall back, don't hard-fail
};

Commits 2177a9f

(AVIF worker abort self-heal + rebuild pkg-avif

) and f8c86d5

(self-heal retry covering core paths) closed the remaining formats.

rav1e parallelizes via rayon too, but its pool doesn't read our custom env pool — it builds its own. Under COI that meant AVIF either ignored our thread budget or hit the same COEP wall. The takeaway: don't assume one rayon pool config applies across codecs. Each WASM crate may own its threading model. [需你核对:rav1e 当前是走全局池还是注入句柄,以代码为准]

crossOriginIsolated

unlocks speed if (crossOriginIsolated) enableThreads

couples two unrelated decisions. Gate on intent, verify the environment.crossOriginIsolated

is false on localhost

and most staging setups. If your threaded path only runs when isolated, you will not exercise it until production. Stand up a COEP-serving preview.cc605f5

).COEP: require-corp

  • COOP: same-origin

on a real preview, not localhost.Cross-Origin-Resource-Policy: same-origin

(or cross-origin

if truly cross-origin).This compressor — and the Turbo path described above — runs in ** zipo.pics**. The full internal postmortem lives in our repo; the architectural facts above are from the fix commits

569731d

, 73e9377

, 2177a9f

, f8c86d5

. Happy to compare notes if you've fought the same COI/COEP war.

── more in #developer-tools 4 stories · sorted by recency
── more on @wasm 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-enabling-cross-o…] indexed:0 read:4min 2026-09-04 ·