Back to Hub
SYSTEM ARCHITECTURE •

The 0ms preview: srcdoc + postMessage, no bundler.

Open CodeSandbox and the preview pipeline looks like this: keystroke → websocket → cloud container → bundler → HMR diff → back to your tab. Every hop adds milliseconds, and a cold container adds seconds. It's a good system. It's also a Rube Goldberg machine for the specific job of showing you what your HTML looks like.

NitroIDE does the same job with one iframe and zero network. Not "low latency" — the network leg is literally 0ms, because there is no network leg. Your code goes from keystroke to pixels inside a single browser tab. This post is the full architecture: how the preview is built, why it never flickers, and the tradeoffs I accepted to get there.

One iframe, not a pipeline

The preview is a single <iframe>. The entire document — your HTML, your CSS in a <style> block, your JS in a <script> block, plus any CDN libraries you've injected — is assembled as one string and assigned to the iframe's srcdoc attribute.

If you haven't used srcdoc much: it tells the browser "parse this string as a complete HTML document," in-process, synchronously. No HTTP request happens. Nothing is fetched, uploaded, or proxied. The first render is just:

// The entire render pipeline: iframe.srcdoc = documentText;

That's the entire reason the network latency is zero. There is no server to have latency with.

The anti-flicker swap

Setting srcdoc on the live frame works, but it has a visible cost: the browser tears down the old document and paints the new one, and between those two moments there's a white flash. On a preview that re-renders constantly, that flash is maddening.

So NitroIDE never re-renders the live frame directly. Instead it builds a second iframe offscreen — absolutely positioned over the live one, opacity: 0, pointer-events: none — sets srcdoc on the hidden frame, and only swaps it in when the load event fires:

// Double-buffering, applied to an iframe const next = document.createElement('iframe'); next.setAttribute('sandbox', current.getAttribute('sandbox')); next.style.cssText = 'position:absolute;inset:0;opacity:0;pointer-events:none'; next.addEventListener('load', () => { current.replaceWith(next); // old frame visible until new one is painted }, { once: true }); next.srcdoc = documentText;

The old frame stays fully visible until the replacement has finished parsing and painting. The user perceives an instant cut, never a blank. It's a double-buffering trick as old as game rendering, applied to an iframe.

Incremental updates: HTML/CSS patch in place, only JS re-runs

Full-document rebuilds are for structural changes. For the thing you actually do 500 times a day — tweak a color, nudge a margin, fix a typo — rebuilding the whole document would reset scroll position, focus, and any JS state. So the editor sends surgical updates through postMessage:

No bundler sits anywhere in this path. Your <script> runs as your script — no transpilation, no module graph, no dev-server transform. That's a deliberate scope decision: NitroIDE is a frontend preview, not a build farm. What you type is what runs, which is exactly why the feedback loop feels instant.

Why incremental matters: a full rebuild is cheap for the machine but expensive for you — it resets scroll, focus, and runtime state. Patching CSS and HTML in place keeps your context alive while you iterate.

The return path: console, errors, and state come back up

A preview that can't talk back is a black box. The iframe reports into the parent over the same postMessage channel: console.log output, runtime errors with line numbers, and the state-watcher's variable snapshots. The parent renders them in the IDE's console panel.

This channel is also where v25's security work lives. As I wrote about in the v25 hardening post, the parent used to interpolate these messages into innerHTML unescaped — a crafted share link could have achieved full parent-origin XSS. The fix was a strict sanitizer that allowlists only the exact HTML vocabulary the console serializer legitimately produces, with everything else escaped or validated. The preview talks freely; the parent trusts nothing. That asymmetry is the whole design.

The sandbox, honestly

The preview iframe runs with sandbox="allow-scripts allow-modals allow-popups allow-forms". Notice what's missing: allow-same-origin. Since v25, the preview runs on an opaque origin — it cannot read the parent's localStorage, where your saved projects live, even if a malicious share link tries.

I'll name the tradeoff plainly: user code in the preview that touches localStorage throws in Safari. That's the correct posture — CodePen and CodeSandbox behave identically — and it's the price of making share links safe by construction rather than by hope. Our docs Troubleshooting section documents this for anyone who hits it.

One-click CDN injection, still zero backend

Mid-session you can inject Tailwind or any CDN library with one click. Implementation-wise it's the boring kind of elegant: the library's <script>/<link> tags are just folded into the srcdoc document string before render. The browser fetches them like any normal page would. No proxy, no bundler plugin, no config file — the thing a bundler would spend 400ms resolving is a string concatenation.

Why this beats the cloud pipeline on latency

CodeSandbox StackBlitz NitroIDE
Where code runs Cloud container WebContainers (WASM) Your tab
Preview transport Websocket + HMR In-browser Node srcdoc string
Network leg Yes No (but WASM boot cost) None
Cold start Seconds Seconds ~0

The honest footnote: "0ms" describes the network leg, not rendering. Layout and paint still cost what they cost in every browser — physics doesn't negotiate. But every avoidable millisecond (round trips, bundler transforms, container wake-ups) is gone, and the avoidable ones were always the big ones.

What it can't do

Intellectual honesty section, because "0ms" invites skepticism:

Try it

NitroIDE is free, no signup, and the preview described above is the actual product: nitroide.com. If you want the security half of this story, the v25 hardening post covers the sandbox design, and the full changelog has the details. Questions about the architecture — there's a newsletter signup in the footer if you want these writeups in your inbox.

Feel the 0ms Preview.

Type and watch it render instantly — no bundler, no websocket, no waiting.

Launch the IDE