Why We Moved Our Interactive Docs to WebContainers (and What Broke)
We replaced CodeSandbox embeds with StackBlitz WebContainers for our component library docs. Cold boot dropped from 45s to 300ms. Here's the integration, the COOP/COEP header requirement, the native addon wall (sharp, bcrypt), and the verification checklist we run before every release.
23 Sept 2025, 09:18 UTC

The Problem: Interactive Examples That Actually Run
Our team maintains a React component library with 200+ interactive examples in our documentation. For years we embedded CodeSandbox iframes—each one spinning up a cloud VM, installing dependencies, and serving a preview. The bill was predictable: ~$400/month for the team plan, plus the occasional "environment unavailable" errors when the platform had incidents.
The real friction wasn't cost. It was latency. A contributor clicking "Edit on CodeSandbox" waited 30-60 seconds for the VM to boot, npm install to finish, and the dev server to start. For a quick typo fix in a prop table, that's a terrible loop.
Thesis: Browser-Native Runtimes Change the Economics of Interactive Docs
StackBlitz's WebContainers runs a real Node.js binary compiled to WebAssembly (wasm32-wasi) inside a Service Worker, with a virtual file system backed by IndexedDB and the Origin Private File System (OPFS). No cloud VM. No network round-trip for file operations. Cold boot in 200-500ms. The trade-off: you lose native addons, raw TCP sockets, and persistent background processes. For documentation examples—mostly Vite, Next.js, or Astro starters—that's an acceptable constraint.
Architecture That Makes It Work
When you open a WebContainer-backed page, the browser streams Wasm modules for Node.js, the VFS, and the TCP stack. A Service Worker intercepts fetch/XMLHttpRequest and routes localhost traffic through an in-browser TCP implementation. The preview iframe communicates via postMessage tunnels, not public ports. Your node_modules lives in OPFS, persisting across reloads without hitting the network.
This means npm install runs the actual npm CLI inside Wasm, populating a real node_modules tree. TypeScript and ESLint language servers run in the same container, giving you full IntelliSense without a remote LSP connection.
Worked Example: Embedding a Live Next.js Example
We replaced a CodeSandbox embed for our "Button" component with a WebContainer using @stackblitz/sdk. The integration lives in our Docusaurus site.
// docs/examples/ButtonLive.tsx
import { WebContainer } from '@stackblitz/sdk';
import { useEffect, useRef } from 'react';
export function ButtonLive() {
const containerRef = useRef<HTMLDivElement>(null);
const wcRef = useRef<WebContainer>();
useEffect(() => {
if (!containerRef.current) return;
WebContainer.boot().then(wc => {
wcRef.current = wc;
// Mount the container's iframe into our DOM
containerRef.current!.appendChild(wc.iframe);
// Write project files
const files = {
'package.json': JSON.stringify({
name: 'button-example',
private: true,
scripts: { dev: 'next dev', build: 'next build' },
dependencies: { next: 'latest', react: 'latest', 'react-dom': 'latest' },
devDependencies: { typescript: 'latest', '@types/react': 'latest' }
}, null, 2),
'pages/index.tsx': `export default function Page() {
return <button className="btn">Click me</button>
}`,
'styles/globals.css': '.btn { padding: 8px 16px; background: #0070f3; color: white; border: none; border-radius: 4px; }'
};
await wc.mount(files);
// Install deps (runs real npm inside Wasm)
const install = await wc.spawn('npm', ['install']);
await install.exit;
// Start dev server
const dev = await wc.spawn('npm', ['run', 'dev']);
dev.output.pipeTo(new WritableStream({
write(chunk) { console.log(new TextDecoder().decode(chunk)); }
}));
// Wait for ready, then navigate iframe to localhost:3000
wc.on('server-ready', (port, url) => {
if (port === 3000) wc.iframe.src = url;
});
});
}, []);
return <div ref={containerRef} style={{ width: '100%', height: '400px' }} />;
}
Where to run: In a Docusaurus/Next.js/Astro MDX page. Permissions: Your site must serve with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers—otherwise SharedArrayBuffer (required for Wasm threads) is blocked. Expected check: Open DevTools → Network → filter "wasm"; you should see node.wasm and vfs.wasm streaming. Risk: If your CDN strips COOP/COEP, the container fails to boot with a cryptic "SharedArrayBuffer not available" error.
What Broke: Native Addons and Memory Ceilings
Our "ImageOptimizer" example uses sharp for runtime image transformations. npm install sharp fails inside WebContainers because node-gyp can't compile C++ to Wasm. The workaround: swap to @img/sharp-wasm32, a prebuilt Wasm binary. Not every package has a Wasm equivalent—bcrypt, canvas, sqlite3 (without better-sqlite3's Wasm build) are dead ends.
Memory is the other ceiling. A monorepo with 15 packages and TurboRepo hit OOM at ~3.2GB in Chrome. The same workspace runs fine in a 8GB cloud VM. For docs examples (typically single-package, <5k files), this hasn't bitten us—but it's a hard limit for full-app reproduction.
Verification Checklist Before You Commit
- Header test:
curl -I https://your-docs-site.com | grep -i cross-origin— confirm COOP/COEP are present. - Offline test: Open the example, disconnect Wi-Fi, edit a file, verify the preview updates. Service Worker cache should serve the dev server.
- Native addon audit: Grep your examples'
package.jsonforsharp|bcrypt|canvas|sqlite3|node-gyp. Each hit needs a Wasm alternative or a redesign. - File count check:
find examples -name 'node_modules' -prune -o -type f -print | wc -l. Over 10k files? Expect slower cold boots.
Closing: Ship the Interactive Version First
WebContainers moved our "time to interactive example" from 45 seconds to under 3. The SDK is open source (@stackblitz/sdk on GitHub), the runtime is free for embedding, and the offline-first model means readers on flaky connections still get a working playground. The constraints—no native addons, 2-4GB RAM, no background persistence—are real but well-documented. For documentation, tutorials, and reproduction repositories, the trade-off favors the browser. Start with one high-traffic example, run the verification checklist, and measure the drop-off rate before and after. That's the metric that justified the migration for us.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.