<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Richang Foo</title>
    <description>The latest articles on DEV Community by Richang Foo (@richang).</description>
    <link>https://dev.to/richang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3021762%2F7c2131eb-ce25-4206-b326-0dde2fc635df.png</url>
      <title>DEV Community: Richang Foo</title>
      <link>https://dev.to/richang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/richang"/>
    <language>en</language>
    <item>
      <title># How enabling cross-origin isolation silently broke our multi-threaded WASM image compressor</title>
      <dc:creator>Richang Foo</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:39:01 +0000</pubDate>
      <link>https://dev.to/richang/-how-enabling-cross-origin-isolation-silently-broke-our-multi-threaded-wasm-image-compressor-3a0p</link>
      <guid>https://dev.to/richang/-how-enabling-cross-origin-isolation-silently-broke-our-multi-threaded-wasm-image-compressor-3a0p</guid>
      <description>&lt;p&gt;&lt;em&gt;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 &lt;code&gt;compression worker crashed&lt;/code&gt;. Here's the root cause and the fix.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;We built an image compressor that runs &lt;strong&gt;100% in the browser&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;For the multi-threaded code paths we rely on &lt;strong&gt;shared memory + atomics&lt;/strong&gt;, which in the browser requires &lt;code&gt;crossOriginIsolated&lt;/code&gt;. So we served the document with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives us &lt;code&gt;crossOriginIsolated === true&lt;/code&gt;, unlocks &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, and lets the &lt;code&gt;*‑threaded&lt;/code&gt; WASM builds actually spawn workers.&lt;/p&gt;

&lt;p&gt;The build uses a nightly toolchain (&lt;code&gt;nightly-2025-06-01&lt;/code&gt; + &lt;code&gt;-Z build-std&lt;/code&gt;) with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RUSTFLAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--cfg=... +atomics,+bulk-memory --shared-memory --import-memory"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a &lt;strong&gt;custom rayon handle pool&lt;/strong&gt; (&lt;code&gt;with_turbo_pool&lt;/code&gt;) instead of &lt;code&gt;build_global&lt;/code&gt;, so we control worker lifecycle and can abort/self-heal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The incident
&lt;/h2&gt;

&lt;p&gt;After flipping COEP to &lt;code&gt;require-corp&lt;/code&gt; in production, &lt;strong&gt;every format started crashing&lt;/strong&gt; with the same message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compression worker crashed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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




&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;*‑threaded&lt;/code&gt; WASM packages spin up &lt;strong&gt;nested rayon workers&lt;/strong&gt; to parallelize the codec. Under COI + COEP &lt;code&gt;require-corp&lt;/code&gt;, those nested workers get blocked by &lt;code&gt;Cross-Origin-Resource-Policy&lt;/code&gt; / 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 &lt;code&gt;compression worker crashed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trap: we had gated the &lt;em&gt;‑threaded&lt;/em&gt; build on &lt;code&gt;crossOriginIsolated&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ the bug: environment detection == feature enable&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crossOriginIsolated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;loadThreadedPackage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// boots nested rayon workers → blocked by COEP&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;strong&gt;enabling COI auto-enabled the broken path&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;(This matches the fix commits &lt;code&gt;569731d&lt;/code&gt; "默认压缩路径禁用 *-threaded 线程包" and &lt;code&gt;73e9377&lt;/code&gt; "COI 下 module worker 被 COEP 拦死".)&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Three changes, all in the loader — not the codec:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Decouple "COI is available" from "use threads."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We no longer load &lt;code&gt;*‑threaded&lt;/code&gt; just because &lt;code&gt;crossOriginIsolated&lt;/code&gt; is true. It loads &lt;strong&gt;only on an explicit opt-in&lt;/strong&gt; — our "Turbo" toggle (&lt;code&gt;v2.compressAccel&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ capability != automatic enable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wantsTurbo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compressAccel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// user/plan explicit&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wantsTurbo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;crossOriginIsolated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;loadThreadedPackage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// default path stays single-threaded and COEP-safe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Keep the single-threaded path as the default and make it robust.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Self-heal on worker crash.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;pool.ts&lt;/code&gt; now catches a dead worker and retries on the safe path, and &lt;strong&gt;aborts Turbo multi-core&lt;/strong&gt; rather than bubbling a hard crash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oncrash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;retryOnSingleThread&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// f8c86d5: 自愈重试覆盖 JPG/PNG/WEBP&lt;/span&gt;
  &lt;span class="nf"&gt;disableTurbo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// fall back, don't hard-fail&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commits &lt;code&gt;2177a9f&lt;/code&gt; (AVIF worker abort self-heal + rebuild &lt;code&gt;pkg-avif&lt;/code&gt;) and &lt;code&gt;f8c86d5&lt;/code&gt; (self-heal retry covering core paths) closed the remaining formats.&lt;/p&gt;




&lt;h2&gt;
  
  
  A second, subtler trap: AVIF / rav1e
&lt;/h2&gt;

&lt;p&gt;rav1e parallelizes via rayon too, but its pool &lt;strong&gt;doesn't read our custom env pool&lt;/strong&gt; — 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. &lt;code&gt;[需你核对：rav1e 当前是走全局池还是注入句柄，以代码为准]&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons (the part worth stealing)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;COI is a double-edged sword.&lt;/strong&gt; &lt;code&gt;crossOriginIsolated&lt;/code&gt; unlocks speed &lt;em&gt;and&lt;/em&gt; tightens loading policy. Anything that spawns workers/sub-resources must satisfy COEP + CORP or it silently dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never let environment detection auto-enable a feature.&lt;/strong&gt; &lt;code&gt;if (crossOriginIsolated) enableThreads&lt;/code&gt; couples two unrelated decisions. Gate on intent, verify the environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test under &lt;em&gt;real&lt;/em&gt; COI.&lt;/strong&gt; &lt;code&gt;crossOriginIsolated&lt;/code&gt; is false on &lt;code&gt;localhost&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-heal, don't hard-crash.&lt;/strong&gt; A worker that can die should degrade to the safe path, not take the whole feature down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability probe, not assumption.&lt;/strong&gt; We now probe for the threaded package and fall back if it fails to boot, instead of assuming presence (&lt;code&gt;cc605f5&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Repro checklist for your own WASM app
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Serve &lt;code&gt;COEP: require-corp&lt;/code&gt; + &lt;code&gt;COOP: same-origin&lt;/code&gt; on a real preview, not localhost.&lt;/li&gt;
&lt;li&gt;[ ] Confirm every worker script response also carries &lt;code&gt;Cross-Origin-Resource-Policy: same-origin&lt;/code&gt; (or &lt;code&gt;cross-origin&lt;/code&gt; if truly cross-origin).&lt;/li&gt;
&lt;li&gt;[ ] Separate "is COI available" from "should I use threads."&lt;/li&gt;
&lt;li&gt;[ ] Add a crash→single-thread fallback in the worker pool.&lt;/li&gt;
&lt;li&gt;[ ] Log which package actually booted, so a COEP block is visible, not silent.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This compressor — and the Turbo path described above — runs in &lt;strong&gt;&lt;a href="https://zipo.pics" rel="noopener noreferrer"&gt;zipo.pics&lt;/a&gt;&lt;/strong&gt;. The full internal postmortem lives in our repo; the architectural facts above are from the fix commits &lt;code&gt;569731d&lt;/code&gt;, &lt;code&gt;73e9377&lt;/code&gt;, &lt;code&gt;2177a9f&lt;/code&gt;, &lt;code&gt;f8c86d5&lt;/code&gt;. Happy to compare notes if you've fought the same COI/COEP war.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>performance</category>
      <category>rust</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
