DEV Community

Sike Ren
Sike Ren

Posted on

Why SharedArrayBuffer forced me to delete every third-party script on my site

I wanted multithreaded ffmpeg running in a browser tab. What I got, several steps
later, was a site with zero third-party scripts — no analytics vendor, no font CDN,
no embedded video, no chat widget. Not because I took a principled stand on privacy.
Because the platform stopped allowing them, and by then it was too late to argue.

Here's the chain, because each link is obvious in isolation and the destination is not.

The chain

ffmpeg is a C program that uses threads. Compiled to WebAssembly via Emscripten,
those threads become Web Workers, and the workers need to share one linear memory
with the main thread. Shared memory in a browser means one thing: SharedArrayBuffer.

SharedArrayBuffer was restricted after Spectre, because a shared buffer plus a
high-resolution timer is a usable side-channel. Browsers brought it back behind a
gate: your document must be cross-origin isolated. That means two response
headers on the document:

  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp
Enter fullscreen mode Exit fullscreen mode

On Cloudflare Pages this is a plain text file at public/_headers, no build step,
no framework config:

    Cross-Origin-Opener-Policy: same-origin
    Cross-Origin-Embedder-Policy: require-corp
Enter fullscreen mode Exit fullscreen mode

Verify it in the console, not by reading your config:

  crossOriginIsolated              // must be true
  typeof SharedArrayBuffer         // 'function'
Enter fullscreen mode Exit fullscreen mode

If crossOriginIsolated is false, ffmpeg.wasm silently falls back to
single-threaded and you spend an afternoon wondering why encoding is slow.

What COEP: require-corp actually does

This is where the third-party scripts died. require-corp says: every cross-origin
subresource must explicitly opt in to being embedded, either by sending
Cross-Origin-Resource-Policy: cross-origin or by being loaded with CORS.

Almost nothing on the open web sends CORP. So:

  • Third-party analytics is gone. A <script src="https://vendor.example/a.js"> is a no-cors request. No CORP header on the response, no script. Every hosted analytics vendor I checked, self-hosted or not, fails this.
  • Google Fonts is gone. The stylesheet request from fonts.googleapis.com is not CORS, so it never loads, so the @font-face rules never arrive.
  • Embeds are gone. A YouTube iframe, a demo video, an embedded widget — iframes are subject to the same rule, and none of them send CORP.
  • COOP: same-origin breaks popup flows too. It severs the window.opener relationship with cross-origin windows, which is exactly the channel that popup-based OAuth uses to hand a token back to you.

There is a softer variant, Cross-Origin-Embedder-Policy: credentialless, which
loads cross-origin no-cors resources without credentials instead of blocking them.
It's the escape hatch if you need third-party images or media — check current
browser support before you build on it. I went with require-corp because I needed
nothing from anyone else.

What still works

Same-origin resources are unaffected: your own fonts, your own images, your own JS.
And crucially, a CORS fetch to your own API is fine — CORS requests satisfy
the opt-in requirement.

So analytics became about twenty lines posting an event name to my own Cloudflare
Worker, and a token-protected plaintext URL to read the counters back. Five events:
page loaded, batch finished, checkout opened, licence verified, encoder failed.
That's enough to tell me which step of the funnel is broken, which is the only
thing I ever actually used a dashboard for.

The part I didn't plan

The tool encodes video locally and uploads nothing. Every site that claims something
like that is asking you to trust a sentence in a footer.

Cross-origin isolation turns it into something you can check in ten seconds: open
DevTools → Network, run a batch, watch it stay empty. Load the page once, go
offline, encode again — it still works, because there's no server in the path.

And the stronger version: I can't quietly leak your data through a third party,
because the browser will not let me load one. The privacy property isn't a promise
I'm making; it's a consequence of a constraint I accepted for an unrelated reason.

Honest costs

  • You own your analytics forever. No Sentry, no session replay, no funnel tool. When something breaks in production you have the events you thought to add, and nothing else.
  • No embedded demo video on your own landing page. Self-host the file or use a GIF.
  • No drop-in chat widget, no support tooling, no A/B testing service.
  • If you genuinely need a third-party resource later, your options are self-host it or proxy it through your own origin. Both are work you didn't budget for.

For a tool whose whole premise is that nothing leaves the tab, that trade was free.
If you're building anything else, understand that turning on SharedArrayBuffer
means adopting all of it — the isolation is not a flag you can scope to one route.

One link

The thing I built with it is BatchCrunch — drop a folder
of video, it compresses all of them locally via ffmpeg.wasm. But the headers above
are the part worth stealing.

Happy to answer anything about the setup, the Emscripten pthread build, or the
worker-side licence check in the comments.

Top comments (0)